I'm trying to create a type that simplifies an existing interface. The existing type comes from @mui/x-data-grid
. The type is
export declare type GridEnrichedColDef<R extends GridValidRowModel = any, V = any, F = V> = GridColDef<R, V, F> | GridActionsColDef<R, V, F>;
The properties I'd like to support are
export type SupportedColumnProps =
| 'field'
| 'headerName'
| 'width'
| 'renderCell'
| 'valueFormatter'
| 'flex'
| 'sortable'
| 'hide'
| 'type'
| 'cellClassName';
as well as the getActions
property if the type
property is set to 'actions'
. But when I try something like Pick<GridEnrichedColDef, SupportedColumnProps | 'getActions'> I can't get the type to recognize that it sometimes should allow
getActions(when
typeis
'actions'`).
How can I create this type to simplify the interface and provide good defaults?
Thanks!
I have the following defined based on @Oblosys's answer:
export type SupportedColumnProps =
| 'field'
| 'headerName'
| 'width'
| 'renderCell'
| 'renderHeader'
| 'valueFormatter'
| 'flex'
| 'sortable'
| 'hide'
| 'type'
| 'cellClassName'
| 'editable'
| 'getActions'
| 'valueOptions';
// https://stackoverflow.com/questions/75271774/creating-a-type-using-pick-with-a-type-that-is-defined-as-a-union-of-types/75290368#75290368
type KeyOfUnion<T> = T extends unknown ? keyof T : never;
type PickUnion<T, K extends KeyOfUnion<T>> = T extends unknown
? K & keyof T extends never
? never
: Pick<T, K>
: never;
export type GridColumn = PickUnion<GridEnrichedColDef, SupportedColumnProps> & { enableColumnMenu?: boolean };
However, now it seems like if I define a column without type
or getActions
, it says my object doesn't satisfy this type. For example:
const column: GridColumn = {
field: 'hello'
}
When applied to a union, Pick
behaves slightly different from what you might expect. One issue is that Pick
has a K extends keyof T
constraint, and for a union, keyof
only returns keys that are present in each union member, so you cannot pick a property that only exists on some of the union members. Moreover, Pick
applied to a union yields a single object with union property values rather than a union of objects (i.e. Pick<{a: number} | {a: string}, 'a'>
evaluates to {a: number | string}
instead of {a: number} | {a: string}
).
To tackle the first problem, you can use a dummy extends
clause to define a distributive conditional type (docs) that applies keyof
to each union member and returns a union of the results:
type KeyOfUnion<T> = T extends unknown ? keyof T : never
type Test = KeyOfUnion<{a: number, b: boolean} | {b: string, c: symbol}>
// type Test = "a" | "b" | "c"
Using KeyOfUnion
in the constraint, you can use another distributive conditional type to apply Pick
to each union member while using an intersection to only pick appropriate keys:
type PickUnionNaive<T, K extends KeyOfUnion<T>> =
T extends unknown ? Pick<T, K & keyof T> : never
type Test = PickUnionNaive<{a: number, b: boolean} | {a: string}, 'a'>
// type Test = PickUnionNaive<{a: number, b: boolean}, "a"> | PickUnion<{a: string}, "a">
// which evaluates to: {a: number} | {a: string}
However, this type has a problem with keys that are not shared by all union members, as these will introduce empty {}
types in the union by picking with never
as the key parameter.
type Test = PickUnionNaive<{a: number, b: boolean} | {a: string}, 'b'>
// type Test = Pick<{a: number, b: boolean}, "b"> | Pick<{a: string}, never>
// which evaluates to: {b: boolean} | {}
This is problematic as it basically turns the resulting union into {}
, and any type can be assigned to {}
. To get rid of the empty objects, you can add an extra conditional to yield never
if intersection between the keys to pick and the keys of the union member is empty, which leads to this definition of PickUnion
:
type PickUnion<T, K extends KeyOfUnion<T>> =
T extends unknown
? K & keyof T extends never ? never : Pick<T, K & keyof T>
: never
type Test = PickUnion<{a: number, b: boolean} | {a: string}, 'b'>
// type Test = {b: boolean}