Er det mulig å få en array-type av rekordens verdier i Typescript?
Jeg vil skaffe alle typene til postens verdier. For eksempel, hvis jeg har et objekt som dette:
{
'a': 1,
'b': false
}
Jeg vil gjerne ha en type som inneholder number og boolean.
Jeg prøver å oppnå en slags funksjonsavbildning uten å miste typer:
type MyContext = { store: number[]; };
const obj: Record<string, (context: MyContext, ...args: any[]) => void> = {
log(ctx: MyContext, msg: string) {
console.log(msg);
},
add(ctx: MyContext, value: number) {
ctx.store.push(value);
}
};
const convert = <TContext>(context: TContext, objectWithFunctions: Record<string, (context: TContext, ...args: any[]) => void>) => {
//omit the first parameter and return a new object with modified functions
const result: Record<string, (...args: any[]) => void> = {};
Object.keys(objectWithFunctions).forEach((functionName) => {
const func = objectWithFunctions[functionName];
result[functionName] = (...args) => func(context, ...args);
});
return result;
};
const context = { store: [] };
const newObj = convert(context, obj);
newObj.log('some message');
newObj.add(12);
newObj.add();//should give an error
newObj.foo();//should give an error
console.log(newObj, context.store);
Lekeplass
Denne implementasjonen fungerer, men den mangler typer fordi any[]-begrensning brukes for det andre argumentet i alle objektets funksjoner. Finnes det en måte å på en eller annen måte utlede typene og returnere en strengere type enn Record<string, (...args: any[]) => void>?
Svar #1
Først og fremst kan du ikke annotere obj som å være av typen Record<string, (context: MyContext, ...args: any[]) => void> uten å kaste bort all informasjon om de enkelte metodenavnene og argumentene i initialiseringen. Hvis du vil at output-typen til convert(context, obj) skal vite om log og add, bør du sannsynligvis bare tilordne til obj uten å annotere i det hele tatt; la kompilatoren utlede typen:
const obj = {
log(ctx: MyContext, msg: string) {
console.log(msg);
},
add(ctx: MyContext, value: number) {
ctx.store.push(value);
}
};
Senere, hvis kompilatoren ikke klager på å kalle convert(context, obj), vet du at obj er av en passende type.
Neste, for at convert() skal sterkt typisere sitt output, må det være generisk ikke bare i T, typen til context, men også i en typeparameter relatert til metode-navn/argument-mapping i objectWithFunctions. Min tilnærming her ville være å gjøre den nye typeparameteren A til en objekttype hvis nøkler er metodenavnene, og hvis verdier er argumentlister som ikke inkluderer den innledende context-parameteren av typen T.
For eksempel, for obj, ville A bli spesifisert som
{
log: [msg: string];
add: [value: number];
}
Merk at du faktisk aldri vil bruke en verdi av typen A, men den vil kunne utledes fra objectWithFunctions-inputen, og det er enkelt å representere både typen til objectWithFunctions og returtypen i form av A. Her er typene:
const convert = <T, A extends Record<keyof A, any[]>>(
context: T,
objectWithFunctions: { [K in keyof A]: (context: T, ...rest: A[K]) => void }
) => {
const result = {} as { [K in keyof A]: (...args: A[K]) => void };
(Object.keys(objectWithFunctions) as Array<keyof A>)
.forEach(<K extends keyof A>(functionName: K) => {
const func = objectWithFunctions[functionName];
result[functionName] = (...args) => func(context, ...args);
});
return result;
};
Så, objectWithFunctions's type er en kartlagt type der arrayet A[K] i hver egenskap K av A blir omgjort til en funksjonstype med et argument av typen T etterfulgt av et rest-argument av typen A[K]. Det viser seg at fordi denne typen har formen {[K in keyof A]...}, er det en homomorf kartlagt type, og kompilatoren er flink til å utlede fra homomorfe kartlagte typer. Returtypen, som gitt i annotasjonen av result, er den samme kartlagte typen uten det innledende T-argumentet.
Inne i selve funksjonen måtte jeg bruke noen typepåstander for å overbevise kompilatoren om at noen verdier hadde bestemte typer. Startverdien til result er et tomt objekt, så jeg måtte påstå at det ville ha den endelige typen (fordi {} absolutt ikke er av den typen). Og returtypen til Object.keys() er bare string[] (se Hvorfor returnerer ikke Object.keys en keyof-type i TypeScript?), så jeg måtte påstå at den returnerer et array av keyof A.
Ok, la oss teste det:
const context: MyContext = { store: [] };
Det er nyttig å annotere context her som MyContext, siden ellers antar kompilatoren at store alltid vil være en tom array (av typen never[]). Og her går vi:
const newObj = convert(context, obj);
Du kan bruke Quickinfo via IntelliSense for å se at kallet til convert() fikk kompilatoren til å anta MyContext for T og den før nevnte { log: [msg: string]; add: [value: number];} typen for A:
/* const convert: <MyContext, {
log: [msg: string];
add: [value: number];
}>(context: MyContext, objectWithFunctions: {
log: (context: MyContext, msg: string) => void;
add: (context: MyContext, value: number) => void;
}) => {
...;
} */
Og når du holder musepekeren over myObj, kan du se at det er akkurat den typen du ønsker at det skal være:
/* const newObj: {
log: (msg: string) => void;
add: (value: number) => void;
} */
Så det oppfører seg som ønsket:
newObj.log('some message');
newObj.add(12);
newObj.add(); // error
newObj.foo(); // error
console.log(newObj, context.store); // {}, [12]
Lenke til kode på lekeplass
