javascript - Hur man härleder värdetypen med hjälp av "punktad väg" inuti interface


Jag vill skapa ett generiskt gränssnitt för tabellkolumn

Och jag vill att TypeScript ska härleda typen av 'value' inuti 'render' genom att använda 'field' (som använder "punkterad sökväg")

Men det drog slutsatsen att 'value' har typen av alla värden i RowData. Vad gör jag fel?

type Paths<T, K extends keyof T = keyof T> = K extends string
    ? T[K] extends Record<string, any>
        ? T[K] extends ArrayLike<any>
            ? `${K}.${Paths<T[K], Exclude<keyof T[K], keyof any[]>>}`
            : `${K}.${Paths<T[K], keyof T[K]>}`
        : K
    : never;

type FieldValue<T, Path extends string> = string extends Path
    ? unknown
    : Path extends keyof T
    ? T[Path]
    : Path extends `${infer K}.${infer R}`
    ? K extends keyof T
        ? FieldValue<T[K], R>
        : unknown
    : unknown;

type Flat<T> = { [k in Paths<T>]: FieldValue<T, k> };

export interface TableColumn<D, K extends keyof Flat<D> = keyof Flat<D>> {
    title: string;
    readonly field?: K;
    render?: (options: { row: D; value: Flat<D>[K] }) => string;
}

interface RowData {
  a: number;
  b: {
    c: string
  }
}

const d: TableColumn<RowData> = {
    title: 'Title',
    field: 'b.c',
    render: ({ value }) => value // value type is string | number but expected string because  RowData['b.c'] is string
};

lekplats

Svar #1

Din definition av

interface TableColumn<D, K extends keyof Flat<D> = keyof Flat<D>> 

ger K-typparametern ett standardvärde på keyof Flat<D>. Om du skriver TableColumn<RowData> betyder det TableColumn<RowData, keyof Flat<D>>. Det betyder inte "vänligen anta den bästa K åt mig"; TypeScript har ingen typparameterinference för generiska typer. Och TableColumn<RowData, keyof Flat<D>> betyder något i stil med

{
   field: "a" | "b.c",
   render?: (options: { row: RowData; value: string | number }) => string;
}

Hoppsan.


Så antingen måste du skriva TableColumn<RowData, "b.c"> explicit, eller så måste du skriva en generisk hjälpfunktion för att få K härlett åt dig, eller så måste du skriva om TableColumn<D> till att vara en unionstyp där varje medlem av unionen motsvarar ett särskilt val av K.

Den här sista är ofta vad folk vill ha, även om du inte kan göra den till ett interface. Låt oss se det:

type TableColumn<D> = { [K in keyof Flat<D>]: {
    title: string;
    readonly field?: K;
    render?: (options: { row: D; value: Flat<D>[K] }) => string;
} }[keyof Flat<D>]

Här har jag gjort TableColumn<D> till en distributiv objekttyp som myntats i ms/TS#47109. En typ av formen {[P in K]: F<P>}[K] distribuerar F<P>-operationen över alla nycklar i K. Och vi vill distribuera din ursprungliga TableColumn<D, K> över alla nycklar i keyof Flat<D>. Här är vad som kommer ut för TableColumn<RowData>:

type RowDataTableColumn = TableColumn<RowData>;
/* type RowDataTableColumn = {
    title: string;
    readonly field?: "a" | undefined;
    render?: ((options: {
        row: RowData;
        value: number;
    }) => string) | undefined;
} | {
    title: string;
    readonly field?: "b.c" | undefined;
    render?: ((options: {
        row: RowData;
        value: string;
    }) => string) | undefined;
} */

Och sedan fungerar saker som du vill:

const d: TableColumn<RowData> = {
    title: 'Title',
    field: 'b.c',
    render: ({ value }) => value // okay
};

Playground-länk till kod