javascript - Begränsa typen av T[K] när man använder K som utvidgar keyof T, samtidigt som en egenskap görs valfri baserat på typen av T[K]
Jag försöker definiera ett gränssnitt med 2 egenskaper:
- Nyckeln (key) för en generisk typ (T)
- Och givet typen av det värde som nyckeln har, en funktion som endast accepterar argument av just den typen (d.v.s. T[K])
Och :
- Det bör säkerställa att egenskap 2 görs valfri när T[K] är en sträng eller ett tal.
I mitt fall tar detta gränssnitt formen av ett konfigurationsobjekt för sortering av tabeller.
interface BaseColumnSortProps<T extends object, K extends keyof T> {
sortField: K;
sort?: (a: T[K], b: T[K]) => number;
}
type ColumnSortProps<T extends object, K extends keyof T> =
T[K] extends string | number ?
BaseColumnSortProps<T, K> : // don't require "sort" for strings and numbers
Required<BaseColumnSortProps<T, K>>; // require custom "sort" for all other types
Och ColumnData innehåller sorteringskonfigurationen:
interface ColumnData<T extends object> {
sort?: ColumnSortProps<T, keyof T>;
}
Med detta gränssnitt bör jag kunna definiera följande funktion:
function testFunction<T extends object>(config: ColumnData<T>[]) {}
Och där T endast innehåller strängar och nummer, bör sort vara valfritt:
interface OnlyStringsAndNumbers {
a: string;
b: number;
}
testFunction<OnlyStringsAndNumbers>([
{ sort: { sortField: "a" } }, // is optional, works ok (no errors)
{ sort: { sortField: "b", sort: (a, b) => a - b } } // but the types for "a" and "b" are a union of all possible types for T[K]
])
Och där T har egna/ohanterade typer, bör sort krävas endast på dessa nycklar:
interface OtherTypes {
a: string;
b: number;
c: boolean;
}
testFunction<OtherTypes>([
{ sort: { sortField: "a" }}, // fails to allow "sort" method to be optional
{ sort: { sortField: "b" }},
{ sort: { sortField: "c" }}
])
Här är en TS Playground-länk på det jag har just nu.
Svar #1
För att detta ska fungera behöver sort-egenskapen hos ColumnData<T> behandla varje egenskap av T individuellt. Du vill inte ha ColumnSortProps<T, keyof T>, eftersom det blandar ihop alla egenskaper på en gång. Istället vill du sprida ColumnSortProps<T, K> över unioner i K. ColumnSortProps<T, K> gör inte det automatiskt.
Ett sätt att göra det är med en distributiv konditionell typ av formen
type DistribColumnSortProps<T extends object, K> =
K extends keyof T ? ColumnSortProps<T, K> : never
interface ColumnData<T extends object> {
sort?: DistribColumnSortProps<T, keyof T>
}
men det visar sig vara knepigt att skriva i linje:
interface ColumnData2<T extends object> {
sort?: keyof T extends infer K ? (
K extends keyof T ? ColumnSortProps<T, K> : never
) : never
}
Ett annat tillvägagångssätt eftersom keyof T är nyckelliknande är att skriva en distributiv objekttyp som beskrivs i microsoft/TypeScript#47109, där vi mappar över keyof T och sedan omedelbart indexerar i den med keyof T, för att få en union:
interface ColumnData<T extends object> {
sort?: { [K in keyof T]: ColumnSortProps<T, K> }[keyof T];
}
Alla tre av dessa tillvägagångssätt resulterar i det önskade beteendet:
testFunction<OnlyStringsAndNumbers>([
{ sort: { sortField: "a" } },
{ sort: { sortField: "b", sort: (a, b) => a - b } } // okay
])
testFunction<OtherTypes>([
{ sort: { sortField: "a" } }, // okay
{ sort: { sortField: "b" } }, // okay
{ sort: { sortField: "c" } } // error,
//~~~~
// Property 'sort' is missing in type '{ sortField: "c"; }'
// but required in type 'Required<BaseColumnSortProps<OtherTypes, "c">>'
])
Lekplatslänk till kod
