Home > other >  How to infer value type iusing using "dotted path" inside interface
How to infer value type iusing using "dotted path" inside interface

Time:04-14

I want to create generic interface for table column

And I want typescript infer type of 'value' inside 'render' by using 'field' (that uses "dotted path")

But it inferred that 'value' has type of all values of RowData What I do wrong ?


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
};

playground

CodePudding user response:

Your definition of

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

gives the K type parameter a default of keyof Flat<D>. If you write TableColumn<RowData>, it means TableColumn<RowData, keyof Flat<D>>. It does not mean "please infer the best K for me"; TypeScript doesn't have type parameter inference for generic types. And TableColumn<RowData, keyof Flat<D>> means something like

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

Oops.


So either you need to write TableColumn<RowData, "b.c"> explicitly, or you need to write a generic helper function to have K inferred for you, or you need to rewrite TableColumn<D> to be a union type where each member of the union corresponds to a particular choice of K.

This last one is often what people want, although you can't make it an interface. Let's see it:

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>]

Here I've made TableColumn<D> a distributive object type as coined in ms/TS#47109. A type of the form {[P in K]: F<P>}[K] distributes the F<P> operation over all the keys in K. And we want to distribute your original TableColumn<D, K> over all the keys in keyof Flat<D>. Here's what comes out for 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;
} */

And then things work how you want:

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

Playground link to code

  • Related