Hvordan slutte seg til typede mapValues ved hjelp av oppslag i TypeScript?


Lignende som:

Hvordan utlede et typet array fra et dynamisk nøkkel-array i TypeScript?

Jeg leter etter å typisere et generisk objekt som mottar et kart av vilkårlige nøkler til oppslagsverdier, og returnerer de samme nøklene med typede verdier (som en typet _.mapValues).

Evnen til å hente en enkelt typet egenskap fra et objekt er dokumentert og fungerer. Med arrays må du hardkode overbelastninger for typede tupler, men for objekter får jeg en 'Duplicate string index signature'-feil.

export interface IPerson {
    age: number;
    name: string;
}

const person: IPerson = {
    age: 1,
    name: ""
}

function getProperty<T, K extends keyof T>(o: T, name: K): T[K] {
    return o[name];
}

const a = getProperty(person, 'age');
// a: number

const n = getProperty(person, 'name');
// n: string

function getProperties<T, K extends keyof T>(obj: T, keys: { [key: string]: K }) {
    const def: { [key: string]: T[K] } = {};
    return Object.entries(keys).reduce((result, [key, value]: [string, K]) => {
        result[key] = getProperty(obj, value);
        return result;
    }, def);
}

const { a2, n2 } = getProperties(person, {
    a2: 'name',
    n2: 'age'
});

// Result:
// {
//     a2: string | number, 
//     n2: string | number
// }

// What I'm looking for:
// {
//     a2: string, 
//     n2: number' 
// }

Hvordan kan dette implementeres med TypeScript?

Svar #1

Så lenge det fungerer under kjøring, kan du fortelle TypeScript hvordan du skal gi nytt navn til nøkler ved hjelp av kartlagte typer:

type RenameKeys<T, KS extends Record<keyof KS, keyof T>> = {[K in keyof KS]: T[KS[K]]};

function getProperties<T, KS extends Record<keyof KS, keyof T>>(
    obj: T,
    keys: KS
): RenameKeys<T, KS> {
    const def = {} as RenameKeys<T, KS>;
    return (Object.entries(keys) as Array<[keyof KS, any]>)
      .reduce((result, [key, value]) => {
        result[key] = getProperty(obj, value);
        return result;
    }, def);
}

Dette bør oppføre seg som forventet i typesystemet. Høydepunkter: typen til keys får en typeparameter kalt KS, som begrenses til å være Record<keyof KS, keyof T>, som mer eller mindre betyr «jeg bryr meg ikke om hva nøklene er, men egenskapstypene må være nøklene fra T». Deretter går RenameKeys<T, KS> gjennom nøklene til KS og henter egenskapstyper fra T som er relatert til dem.

Til slutt måtte jeg gjøre noen typepåstander... def er RenameKeys<T, KS>. Typen til value i [key, value] gjorde jeg nettopp til any, siden det er vanskelig for typesystemet å verifisere at result[key] vil ha riktig type. Så det er litt av en tildekking av implementasjonens typesikkerhet... men den som kaller getProperties() bør være fornøyd:

const {a2, n2} = getProperties(person, {
  a2: 'name',
  n2: 'age'
});
// a2 is string, n2 is number.

Lenke til kode på lekeplassen

Svar #2

Det kan være en måte å optimalisere/sammenkollapse denne typingen litt, men jeg har en funksjonsprototype for getProperties som jeg tror oppnår det du leter etter.

Det som mangler i definisjonen din er en sterk returtype-definisjon som knytter sammenhengen mellom en spesifikk nøkkel i keys-objektet og den spesifikke typen i obj-objektet. Fordi dette mangler, blir alt en forening av typer, som er oppførselen du ser ovenfor.

Funksjonstypen jeg har kommet opp med er:

function getProperties
    <T, K extends keyof T, U extends { [name: string]: K }>
        (obj: T, keys: U):
            {[V in keyof U]: T[U[V]];

Den viktige delen her er returverditypen: {[V in keyof U]: T[U[V]]}

Den spesifiserer at for hver nøkkel V i keys-objektet:

  1. V vil være en nøkkel i utdataobjektet

  2. Verditypen vil være en type fra input obj, der typen kommer fra nøkkelen gitt av verdien assosiert med nøkkelen V i U.

Relaterte artikler