Dynamiskt extrahera typ i TypeScript
Jag försöker dynamiskt kartlägga en typ till en annan baserat på vissa flaggor, vilket kan resultera i valfria fält.
type Constraint = {
required: boolean,
callback: () => any,
}
type Schema = Record<string, Constraint>;
const mySchema: Schema = {
bar: {
required: true,
callback: () => 1
},
foo: {
required: false,
callback: () => true
}
}
type MapSchemaToOutput<T extends Schema> = {
[K in keyof T as T[K]['required'] extends true ? K : never]: ReturnType<T[K]['callback']>
} & {
[K in keyof T as T[K]['required'] extends false ? K : never]?: ReturnType<T[K]['callback']>
}
type Output = MapSchemaToOutput<typeof mySchema>;
Slutmålet är att ha Output lika med:
{
bar: number,
foo?: boolean
}
Jag vet att jag kan göra kartläggningen för hand, intresserad av att veta om detta kan göras dynamiskt.
Svar #1
Ditt tillvägagångssätt fungerar som det är, med en ändring.
Problemet är att : Schema-annoteringen "kastar bort typinformationen":
const mySchema: Schema = {
//...
};
Med den anteckningen kommer TS bara ihåg att mySchema är Record<string, Constraint>, inte någon av objektets specifika strukturer.
En lösning är as const:
const mySchema = {
//...
} as const;
Detta bevarar de bokstavliga typerna inom objektet. Däremot finns det inte längre några begränsningar på innehållet i mySchema, och eventuella fel vid definitionen av mySchema måste fångas upp vid användning, snarare än vid definitionstidpunkt.
En bättre lösning är att använda en hjälpfunktion för att införa en begränsning, utan att direkt ange typen:
function buildSchema<T extends Schema>(schema: T) { return schema; }
const mySchema = buildSchema({
//...
});
På grund av begränsningen <T extends Schema> kommer TS att ge ett fel, precis som tidigare, om schemaobjektet inte matchar den angivna typen.
Men till skillnad från att annotera objektets typ, är denna typ som returneras av denna funktion oförändrad från det bokstavliga objekt som förs till funktionen: så ingen typinformation går förlorad.
Med denna förändring fungerar resten av typerna som förväntat
Svar #2
Din MapSchemaToOutput-typ är faktiskt korrekt givet den lämpliga T-typen. Dock har den vissa begränsningar när vi tillämpar den på typeof mySchema.
Spela in
Schema är ett Record vilket betyder enligt definition att dess nycklar är varje string. Vi förlorar möjligheten att se de specifika nycklar som faktiskt är närvarande.
Din karttyp är okej på grund av extends. Men vi vill inte tillämpa typen Schema på variabeln mySchema. Vi behöver få en mer specifik typ för den.
Booleska typer
Typen för boolean-värdena true och false kommer generellt att härledas som boolean istället för deras bokstavliga värden. Om typen av T[K]['required'] är boolean så utökar den inte true eller false, så den kommer inte att uppfylla någon av villkoren i mappen.
Jag rekommenderar att ta bort extends-kontrollen för de valfria egenskapsnycklarna så att alla egenskaper kommer att inkluderas som valfria som standard. Att inkludera kravvärden på båda ställena är inte ett problem eftersom de är sammanslagna med &, så de måste finnas för att uppfylla båda villkoren.
as const & readonly
För att få de bokstavliga boolean-värdena av mySchema behöver vi använda as const. Detta tolkar alla värden som bokstavliga. Det gör också typen readonly och den mappade utdata kommer också att bli readonly. Vi kan ta bort readonly genom att lägga till -readonly till nycklarna i typen MapSchemaToOutput.
Sätter man ihop allt detta får vi:
type MapSchemaToOutput<T extends Schema> = {
-readonly[K in keyof T as T[K]['required'] extends true ? K : never]: ReturnType<T[K]['callback']>
} & {
-readonly[K in keyof T]?: ReturnType<T[K]['callback']>
}
type Output = MapSchemaToOutput<typeof mySchema>;
löser sig till:
type Output = {
bar: number;
} & {
bar?: number | undefined;
foo?: boolean | undefined;
}
Typescript Playground-länk
