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