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:

  1. Nyckeln (key) för en generisk typ (T)
  2. 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 :

  1. 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