Search code examples
typescriptmaterial-uimui-x

Creating a type using Pick with a type that is defined as a union of types


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(whentypeis'actions'`).

How can I create this type to simplify the interface and provide good defaults?

Thanks!

Update 1/30/2023

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

enter image description here


Solution

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

    TypeScript playground