Using typescript
how can i make "value" parameter in "formatValue" function to be the type of keyOf T
?
export interface ICol<T extends Record<string, any>, K extends keyof T = keyof T> {
accessor?: K;
formatValue?: (value: T[K]) => JSX.Element | string | number;
}
const col: ICol<{ a: number; b: string; c: Date; }> = {
accessor: 'a',
formatValue: (value) => {
return `Value is ${value}`;
},
};
The tooltip must say value:number
when accessor is "a" but value:string
when accessor is "b". The current tooltip say: (parameter) value: string | number | date
I need value to be number in this case.
Thanks.
You want ICol<{ a: number; b: string; c: Date; }>
without a second generic type argument K
to be equivalent to the union of ICol<{ a: number; b: string; c: Date; }, K>
for each K
in keyof T
. That is, you want to distribute a union in K
over the ICol<T>
operation. There are two relatively straightforward ways to do that.
One is to use a distributive conditional type like this:
type ICol<T extends Record<string, any>, K extends keyof T = keyof T> =
K extends unknown ? {
accessor?: K;
formatValue?: (value: T[K]) => JSX.Element | string | number;
} : never
Because K
is a generic type parameter, the conditional type K extends unknown ? ⋯ : never
automatically distributes the ⋯
over unions in K
. We're not really trying to check anything (K extends unknown
is always true), just use distributive conditional type. That results in:
type Z = ICol<{ a: number; b: string; c: Date; }>;
/* type Z = {
accessor?: "a" | undefined;
formatValue?: ((value: number) => JSX.Element | string | number) | undefined;
} | {
accessor?: "b" | undefined;
formatValue?: ((value: string) => JSX.Element | string | number) | undefined;
} | {
accessor?: "c" | undefined;
formatValue?: ((value: Date) => JSX.Element | string | number) | undefined;
} */
And then you get the behavior you expected where value
is inferred as number
:
const col: ICol<{ a: number; b: string; c: Date; }> = {
accessor: 'a',
formatValue: (value) => {
return `Value is ${value}`;
},
};
The other is to use a distributive object type as coined in microsoft/TypeScript#47109, where you make a mapped type over the union members of K
and then immediately index into that mapped type with K
:
type ICol<T extends Record<string, any>, K extends keyof T = keyof T> = { [P in K]: {
accessor?: P;
formatValue?: (value: T[P]) => JSX.Element | string | number;
} }[K]
Here we declare a type parameter P
to iterate over the elements of K
. This produces a mapped type with the same keys as T
but whose properties are the object types we're looking for. When we index into this mapped type with K
, we get the union of those object types.
So the same output happens:
type Z = ICol<{ a: number; b: string; c: Date; }>;
/* type Z = {
accessor?: "a" | undefined;
formatValue?: ((value: number) => JSX.Element | string | number) | undefined;
} | {
accessor?: "b" | undefined;
formatValue?: ((value: string) => JSX.Element | string | number) | undefined;
} | {
accessor?: "c" | undefined;
formatValue?: ((value: Date) => JSX.Element | string | number) | undefined;
} */
These types still let you pass in a type for K
if you don't want to use the default keyof T
, so you can get a more specific type if you want:
type X = ICol<{ a: number; b: string; c: Date; }, "a">;
/* type X = {
accessor?: "a" | undefined;
formatValue?: ((value: number) => JSX.Element | string | number) | undefined;
} */
If you don't need that ability you can rewrite ICol<T>
to remove the second type parameter:
type ICol<T extends Record<string, any>> =
keyof T extends infer K ? K extends keyof T ? {
accessor?: K;
formatValue?: (value: T[K]) => JSX.Element | string | number;
} : never : never;
or
type ICol<T extends Record<string, any>> = { [P in keyof T]: {
accessor?: P;
formatValue?: (value: T[P]) => JSX.Element | string | number;
} }[keyof T]
Note that since ICol<T>
needs to at least sometimes be a union type, you cannot make ICol<T>
an interface. Interfaces can only represent single object types (or intersections of object types) with statically-known members.