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

Relaterte artikler