Hur man skriver en TypeScript-typ som extraherar egenskaper från en annan typ med hjälp av en lista av nycklar


Jag försöker skriva en TypeScript-typ som omvandlar en tuppel av nycklarna i en typ till en typ med endast de egenskaperna. Följande kod är det närmaste jag har kommit:

type Primitive = string | number | boolean;
type ExtractProps<TObject, TKeys extends (keyof TObject)[], TExtends = any> = {
    [Index in keyof TKeys as TKeys[Index] extends keyof TObject ? (TObject[TKeys[Index]] extends TExtends ? TKeys[Index] : never) : never ]:
        TKeys[Index] extends keyof TObject ? TObject[TKeys[Index]] : never 
};

type Type = {
    'string': string;
    'number': number;
    'boolean': boolean;
    'array': Array<any>
}

type Test1 = ExtractProps<Type, ['string', 'number', 'array']>;
// Output:
// type Test1 = {
//     string: string | number | any[];
//     number: string | number | any[];
//     array: string | number | any[];
// }
// Desired Output:
// type Test1 = {
//     string: string;
//     number: number;
//     array: any[];
// }
type Test2 = ExtractProps<Type, ['string', 'number', 'array'], Primitive>;
// Output:
// type Test2 = {
//     string: string;
//     number: number;
// }
// Desired Output:
// type Test2 = {
//     string: string;
//     number: number;
// }
type Test3 = ExtractProps<Type, ['string', 'number'], Primitive>;
// Output:
// type Test3 = {
//     string: string | number;
//     number: string | number;
// }
// Desired Output:
// type Test3 = {
//     string: string;
//     number: number;
// }

Jag kan bara få typen att fungera korrekt när omkartläggning uttrycket utvärderas till never för en av nycklarna (som array i Type2 eftersom egenskapstypen inte är en utvidgning av Primitive). Finns det ett sätt att skriva ExtractProps så att Type1 och Type3 har de korrekta egenskapstyperna?

Svar #1

Det du letar efter verkar vara en kombination av verktygstypen Pick<T, K> som producerar en typ från T med endast en delmängd av de kända egenskaperna, specifikt de vars nycklar finns i K, och något vi kan kalla PickByValue<T, V> (som efterfrågats i Hur man skriver typen PickByValue?) som producerar en typ från T med endast en delmängd av de kända egenskaperna, specifikt de vars värden finns i V. Vi kan använda nyckelomvandling via as för att skriva PickByValue:

type PickByValue<T, V> =
    { [K in keyof T as T[K] extends V ? K : never]: T[K] };

Och sedan kombinera dem för att få ExtractProps:

type ExtractProps<T, K extends keyof T, V = any> =
    PickByValue<Pick<T, K>, V>

Låt oss testa det:

type Test1 = ExtractProps<Type, 'string' | 'number' | 'array'>;
/* type Test1 = {
    string: string;
    number: number;
    array: any[];
} */

type Test2 = ExtractProps<Type, 'string' | 'number' | 'array', Primitive>;
/* type Test2 = {
    string: string;
    number: number;
} */


type Test3 = ExtractProps<Type, 'string' | 'number', Primitive>;
/* type Test3 = {
    string: string;
    number: number;
} */

Ser bra ut.


Observera att du även kan implementera ExtractProps direkt, genom att kombinera funktionaliteten hos Pick och PickByValue utan att använda dem:

type ExtractProps<T, K extends keyof T, V = any> =
    { [P in keyof T as T[P] extends V ? Extract<K, P> : never]: T[P] 
} 

Och slutligen, notera att, som det står i frågan, vill din version av ExtractProps att dess andra argument ska vara en arrayliknande typ vars element är nycklar. Du skulle kunna göra det:

type ExtractPropsArray<T, KS extends Array<keyof T>, V = any> =
    PickByValue<Pick<T, KS[number]>, V>;

men implementeringen bryr sig inte om detaljerna i array-typen utöver föreningen av dess elementtyper. Den kastar bara bort större delen av informationen om KS. Jag skulle inte rekommendera att skriva en typ som kräver information den egentligen inte behöver. Du kan alltid packa upp arrayen innan du använder den, som ExtractProps<T, KS[number], V>, medan det att uppfinna en värdelös array-wrapper när du börjar med en keylike-typ är lite fånigt, som ExtractPropsArray<T, K[], V> eller ExtractPropsArray<T, [K, K], V>. Men det är i slutändan mer en åsikt än en auktoritativ källa, så du bör göra det som du tycker är lämpligt.

Lekplatslänk till kod