Er det muligt at få en array-typer af rekordens værdier i Typescript?
Jeg vil opnå alle typerne af postens værdier. For eksempel, hvis jeg har et objekt som dette:
{
'a': 1,
'b': false
}
Jeg vil gerne have en type, som indeholder number og boolean.
Jeg forsøger at opnå en slags funktionskortlægninger uden at 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);
Legeplads
Denne implementering fungerer, men den mangler typer, fordi any[]-begrænsningen bruges for det andet argument i alle objektets funktioner. Er der en måde at på en eller anden måde udlede typerne og returnere en mere streng type i stedet for Record<string, (...args: any[]) => void>?
Svar #1
Først og fremmest kan du ikke annotere obj som værende af typen Record<string, (context: MyContext, ...args: any[]) => void> uden at kassere al information om de specifikke metodenavne og argumenter i initialiseringen. Hvis du vil have outputtypen af convert(context, obj) til at vide om log og add, bør du sandsynligvis bare tildele til obj uden at annotere overhovedet; lad compileren inferere dens type:
const obj = {
log(ctx: MyContext, msg: string) {
console.log(msg);
},
add(ctx: MyContext, value: number) {
ctx.store.push(value);
}
};
Senere, hvis compileren ikke klager over at kalde convert(context, obj), så ved du, at obj er af en passende type.
Dernæst, for at convert() kan stærkt typificere sit output, skal det være generisk ikke kun i T, typen af context, men også i en typeparameter relateret til metode-navn/argument mapping i objectWithFunctions. Min tilgang her ville være at gøre den nye typeparameter A til en objekttype, hvis nøgler er metodenes navne, og hvis værdier er argumentlisterne uden den oprindelige context-parameter af typen T.
For eksempel, for obj, ville A blive angivet som
{
log: [msg: string];
add: [value: number];
}
Bemærk, at du aldrig faktisk vil bruge en værdi af typen A, men den vil kunne udledes fra objectWithFunctions-inputtet, og det er ligetil at repræsentere både typen af objectWithFunctions og returtypen i form af A. Her er typingerne:
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 mappet type, hvor arrayet A[K] i hver egenskab K af A bliver transformeret til en funktionstype med et argument af typen T efterfulgt af et rest-argument af typen A[K]. Det viser sig, at fordi denne type er af formen {[K in keyof A]...}, er det en homomorf mappet type, og compileren er god til at inferere fra homomorfe mappede typer. Returtypen, som angivet i annoteringen af result, er den samme mappede type uden det indledende T-argument.
Inde i funktionen var jeg nødt til at bruge nogle typepåstande for at overbevise compileren om, at nogle værdier havde bestemte typer. Den oprindelige værdi af result er et tomt objekt, så jeg måtte hævde, at det ville være den endelige type (fordi {} bestemt ikke er af den type). Og returtypen af Object.keys() er bare string[] (se Hvorfor returnerer Object.keys ikke en keyof-type i TypeScript?), så jeg måtte hævde, at det returnerer et array af keyof A.
Okay, lad os teste det:
const context: MyContext = { store: [] };
Det er nyttigt at annotere context her som MyContext, da compileren ellers antager, at store altid vil være et tomt array (af typen never[]). Og her går vi:
const newObj = convert(context, obj);
Du kan bruge Quickinfo via IntelliSense til at se, at kaldet til convert() fik compileren til at udlede MyContext for T og den førnævnte { log: [msg: string]; add: [value: number];} type 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 musen over myObj, kan du se, at det præcis er den type, du ønsker, at det skal være:
/* const newObj: {
log: (msg: string) => void;
add: (value: number) => void;
} */
Så det opfører sig som ønsket:
newObj.log('some message');
newObj.add(12);
newObj.add(); // error
newObj.foo(); // error
console.log(newObj, context.store); // {}, [12]
Playground-link til kode
