javascript - TypeScript: Posisjonelle ELLER Navngitte parametere i konstruktør
Jeg har en klasse som for øyeblikket tar 7+ posisjonelle parametere.
class User {
constructor (param1, param2, param3, …etc) {
// …
}
}
Jeg vil konvertere dette til navngitte parametere via et alternativsobjekt.
type UserOptions = {
param1: string
// …
}
class User {
constructor ({ param1, param2, param3, …etc } = {}: UserOptions) {
// …
}
}
Dette er greit, bortsett fra at det er mange tester som nå må omarbeides for å endre signaturen, så jeg vil gjerne støtte både navngitte og posisjonelle parametere.
Jeg kan få koden til å støtte begge, men jeg er ikke sikker på hvordan jeg får typene inn der uten å skrive ut alle typene to ganger. Ideelt sett ville listen over typer i min UserOptions bli til posisjonelle parametere i den rekkefølgen de er definert i UserOptions.
Er det en måte å gjøre noe sånt på?
type UserOptions = {
param1: string
// …
}
type ToPositionalParameters<T> = [
// ??? something like ...Object.values(T)
// and somehow getting the keys as the positional argument names???
]
type PositionalUserOptions = ToPositionalParameters<UserOptions>
class User {
constructor (...args: PositionalUserOptions);
constructor (options: UserOptions);
constructor (...args: [UserOptions] | PositionalUserOptions) {
// …
}
}
Ville også vurdert en løsning som fungerer den andre veien, altså fra posisjonelle argumenter til navngitte, kanskje dette er lettere?
Svar #1
Det er noen få ting som blokkerer deg her.
Det første er at rekkefølgen på egenskaper i en objekttype for øyeblikket er ikke observerbar i TypeScript, fordi de ikke påvirker tildelbarhet. For eksempel er det ingen forskjell mellom typen {a: string, b: number} og {b: number, a: string} i typesystemet. Det finnes skitne triks du kan utføre for å trekke informasjon ut av kompilatoren for å se om den representerer nøklene som ["a", "b"] vs ["b", "a"], men alt det gjør er å gi deg noen orden når du kompilerer; det er ikke garantert at det er rekkefølge når du leser typen deklarasjonen ovenfra og ned. .. pokker, det er ikke engang garantert at det er samme rekkefølge hver gang du kompilerer! Se microsoft/TypeScript#17944 for en åpen sak som ber om at det skal være en konsistent rekkefølge. Og se microsoft/TypeScript#42178 for et eksempel på en tilsynelatende irrelevant kodeendring som endrer rekkefølgen fra det som forventes. Foreløpig kan vi ikke automatisk gjøre om en objekttype til en ordnet tuple på en konsistent måte.
Det andre er at navnene på argumenter i en funksjonstype bevisst ikke er tilgjengelige som strengbokstavertyper. De er ikke observerbare, av lignende grunner som for objekt-egenskapers rekkefølge: de påvirker ikke tildelbarhet. For eksempel er det ingen forskjell mellom funksjonstypen (foo: string) => void og (bar: string) => void i typesystemet. Jeg tror ikke engang at det finnes noen triks som kan avsløre slike detaljer. I typesystemet er funksjonsargumentnavn kun nyttige som dokumentasjon eller IntelliSense. Du kan konvertere mellom navngitte funksjonsargumenter og merkede tuple-elementer, men det er stort sett det. Se denne kommentaren i microsoft/TypeScript#28259 som forklarer at vi ikke kan bruke kallsignatur-parameternavn eller tuple-etiketter som strenger i TypeScript. For nå kan vi ikke automatisk gjøre en merket tuple om til en objekttype der nøklene i objektet tilsvarer etikettene i tuplen.
Du kan på en måte omgå begge disse problemene hvis vi, i stedet for å prøve å konvertere et objekt til en tuppel eller en tuppel til et objekt, gir nok informasjon til å gjøre begge deler:
const userOptionKeys = ["param1", "param2", "thirdOne"] as const;
type PositionalUserOptions = [string, number, boolean];
Her userOptionKeys er en eksplisitt ordnet liste over de ønskede nøklene i UserOptions-objektene... samme rekkefølge som i vår manuelt opprettede PositionalUserOptions-tuple. Nå som vi har nøkkelnavn, kan vi bygge UserOptions:
type UserOptions = { [I in Exclude<keyof PositionalUserOptions, keyof any[]> as
typeof userOptionKeys[I]]: PositionalUserOptions[I] }
/* type UserOptions = {
param1: string;
param2: number;
thirdOne: boolean;
} */
Og mens vi først er i gang, kan vi skrive en funksjon for å konvertere en tuple av typen PositionalUserOptions til et objekt av typen UserOptions (med en any-typepåstand for å frigjøre kompilatoren fra å måtte forsøke å verifisere det, noe den ikke lett kan gjøre):
function positionalToObj(opts: PositionalUserOptions): UserOptions {
return opts.reduce((acc, v, i) => (acc[userOptionKeys[i]] = v, acc), {} as any)
}
Nå kan vi skrive User-klassen, og bruke positionalToObj i implementeringen av konstruktøren for å normalisere ting:
class User {
constructor(...args: PositionalUserOptions);
constructor(options: UserOptions);
constructor(...args: [UserOptions] | PositionalUserOptions) {
const opts = args.length === 1 ? args[0] : positionalToObj(args);
console.log(opts);
}
}
new User("a", 1, true);
/* {
"param1": "a",
"param2": 1,
"thirdOne": true
} */
Det fungerer! Fra et typesystem- og tildelingssynspunkt er dette det beste du kan gjøre. Fra et dokumentasjons-/IntelliSense-perspektiv, er det ikke så bra. Når du kaller new User(), vil dokumentasjonen for multi-param-versjonen gi deg etiketter som args_0 og args_1. Hvis du vil at disse skal være param1 og param2, må du bare bite i det sure eplet og skrive ut parameterne to ganger; en gang som streng-litteraler, og en gang som tuple-etiketter, siden det ikke finnes en måte å konvertere den ene til den andre på:
const userOptionKeys = ["param1", "param2", "thirdOne"] as const;
type PositionalUserOptions = [param1: string, param2: number, thirdOne: boolean];
Er det verdt det? Kanskje... det er opp til deg.
Lenke til kode på lekeplassen
Svar #2
Du kan gjøre dette, men verktøyene du må bruke er frarådet av TypeScript-teamet og avhenger av instansieringsrekkefølgen til visse interne typer (i det minste er det det problemet som ber om en union-til-tuple-operasjon antyder).
Likevel var det morsomt å finne det ut, så her har du det:
// Via https://github.com/Microsoft/TypeScript/issues/13298#issuecomment-707364842
type UnionToTuple<T> = (
(
(
T extends any
? (t: T) => T
: never
) extends infer U
? (U extends any
? (u: U) => any
: never
) extends (v: infer V) => any
? V
: never
: never
) extends (_: any) => infer W
? [...UnionToTuple<Exclude<T, W>>, W]
: []
);
type Head<T> = T extends [infer A, ...any] ? A : never
type Tail<T extends any[]> = T extends [any, ...infer A] ? A : never;
type PluckFieldTypes<T extends object, Fields extends any[] =
UnionToTuple<keyof T>> = _PluckFieldType<T, Head<Fields>, Tail<Fields>, []>
type _PluckFieldType<
T extends object,
CurrentKey,
RemainingKeys extends any[],
Result extends any[]
> = RemainingKeys['length'] extends 0
? CurrentKey extends keyof T ? [...Result, T[CurrentKey]] : never
: CurrentKey extends keyof T
/* && */? RemainingKeys extends (keyof T)[]
? [...Result, T[CurrentKey], ..._PluckFieldType<T, Head<RemainingKeys>, Tail<RemainingKeys>, []>]
: never : never;
// -- IMPL --
type Args = {
param1: string,
param2: number,
param3: Date,
param4: number,
param5: string,
param6: Date,
param7: boolean,
param8: boolean,
param9: null,
param10: 'abc' | 'xyz'
}
class CompositeClass {
constructor(params: Args);
constructor(param1: string, param2: number, param3: Date);
constructor(...args: [Args] | PluckFieldTypes<Args>) {
}
}
Den utledede typen til ...args ender opp med å være (parameter) args: [Args] | [string, number, Date, number, string, Date, boolean, boolean, null, "abc" | "xyz"] og den andre konstruktøren får en rød bølgete strek under seg til du legger inn alle de andre parameterne.
Det eneste vi ikke kan få, er et krav om at navnene på argumentene må samsvare med navnene på feltene (men det er fordi merkede tupler ikke gjør etikettene sine tilgjengelige på typenivå, så du kan ikke lese dem fra den innebygde ConstructorArguments eller skrive dem her med _PluckFieldType.)
