Search code examples
typescripttypescript-genericsunion-types

keyof Model not assignable to union of keys issue in generic type


I tried to make strictly typed column builder and ran into union type issue. I think the solution is to infer builder return type as (ColumnDef<Model, "Key1"> | ColumnDef<Model, "Key2>)[], but don't know how to achieve this

...

type EnhancedCellRenderer<
  Model extends AnyObject,
  Field extends keyof Model = keyof Model & string
> = (props: EnhancedCellRenderProps<Model, Field>) => ReactNode

type CellRenderer<
  Model extends AnyObject,
  Field extends keyof Model = keyof Model & string
> = BasicCellRenderer<Model[Field]> | EnhancedCellRenderer<Model, Field>

interface ColumnDef<
  Model extends AnyObject,
  Field extends keyof Model = keyof Model & string
> {
  width?: number | string
  field: Field
  renderer?: CellRenderer<Model, Field>
}

type ColumnBuilder<Model extends AnyObject> = <Field extends keyof Model>(
  field: Field,
  config?: Omit<ColumnDef<Model, Field>, 'field'>
) => ColumnDef<Model, Field>

declare function buildColumns<Model extends AnyObject>(
  cb: (builder: ColumnBuilder<Model>) => ColumnDef<Model>[]
): ColumnDef<Model>[]

type Model = {
  '01.20': number
  '02.05': number
  '02.20': number
  '03.05': number
  '03.20': number
  '04.05': number
  '04.20': number
}

buildColumns<Model>((builder) => [builder('01.20'), builder('02.05')])

I've got this error

Type 'ColumnDef<Model, "01.20">' is not assignable to type 'ColumnDef<Model, "01.20" | "02.05" | "02.20" | "03.05" | "03.20" | "04.05" | "04.20">'.
  Types of property 'renderer' are incompatible.
    Type 'CellRenderer<Model, "01.20"> | undefined' is not assignable to type 'CellRenderer<Model, "01.20" | "02.05" | "02.20" | "03.05" | "03.20" | "04.05" | "04.20"> | undefined'.
      Type 'EnhancedCellRenderer<Model, "01.20">' is not assignable to type 'CellRenderer<Model, "01.20" | "02.05" | "02.20" | "03.05" | "03.20" | "04.05" | "04.20"> | undefined'.
        Type 'EnhancedCellRenderer<Model, "01.20">' is not assignable to type 'EnhancedCellRenderer<Model, "01.20" | "02.05" | "02.20" | "03.05" | "03.20" | "04.05" | "04.20">'.
          Type '"01.20" | "02.05" | "02.20" | "03.05" | "03.20" | "04.05" | "04.20"' is not assignable to type '"01.20"'.

Typescript Playground


Solution

  • Naming note: generic type parameters are conventionally given short names of one or two uppercase characters, so as to more easily distinguish them from specific type names. For example, it is confusing to see Model be both a specific type and a generic type parameter. Therefore in what follows I will use M and F instead of Model and Field, when used as type parameters.


    I can't claim to have gone through your code closely enough to understand what it's actually trying to do. Still, I get the sense that you don't really want ColumnDef<M> to be a single thing that works for the full union of fields, but rather a union of things that work for a single field each. If so, then you can change ColumnDef to distribute across unions in its F argument:

    type ColumnDef<M extends AnyObject, F extends keyof M = keyof M> =
      F extends keyof M ? {
        width?: number | string
        field: F
        renderer?: CellRenderer<M, F>
      } : never;
    

    If you inspect ColumnDef<Model>, you'll see that it is now a union type:

    type ColumnDefModel = ColumnDef<Model>;
    /* type ColumnDefModel = {
        width?: string | number | undefined;
        field: "01.20";
        renderer?: CellRenderer<Model, "01.20"> | undefined;
    } | {
        width?: string | number | undefined;
        field: "02.05";
        renderer?: CellRenderer<...> | undefined;
    } | ... 4 more ... | {
        ...;
    } */
    

    And your buildColumns() call is no longer in error:

    buildColumns<Model>((builder) => [builder('01.20'), builder('02.05')])
    

    Playground link to code