Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

TypeScript keyof index type is too wide

Apologies, I'm sure this has been answered somewhere, but I'm not sure what to google. Please do edit my question if the terms in the title are wrong.

I have something like this:

type RowData = Record<string, unknown> & {id: string}; 


type Column<T extends RowData, K extends keyof T> = {  
  key: K; 
  action: (value: T[K], rowData: T) => void;
}

type RowsAndColumns<T extends RowData> = {
  rows: Array<T>; 
  columns: Array<Column<T, keyof T>>; 
}

And TypeScript should be able to infer the types of the action functions, by examining the shape of the rows, and what key value has been given to the column:

ie:

function myFn<T extends RowData>(config: RowsAndColumns<T>) {

}

myFn({
  rows: [
    {id: "foo", 
      bar: "bar", 
      bing: "bing", 
      bong: {
        a: 99, 
        b: "aaa"
      }
    }
  ], 
  columns: [
    {
      key: "bar", 
      action: (value, rowData) => {
          console.log(value);
      }
    },
     {
      key: "bong", 
      action: (value, rowData) => {
          console.log(value.a); //Property 'a' does not exist on type 'string | { a: number; b: string; }'.

      }
    }
  ]
}); 

Playground Link

The problem is, TypeScript seems to be deriving the type of value (value: T[K]) as 'the types of all accessible by all keys of T' rather than using just the key provided in the column object.

Why is TypeScript doing this, and how can I solve it?

What would make some good answer is defining some specific terms and concepts.

I imagine I want to change my K extends keyof T to be something like 'K is a keyof T, but only one key, and it never changes'.

like image 460
dwjohnston Avatar asked Nov 09 '20 01:11

dwjohnston


1 Answers

If you expect the keys of T to be a union of literals like "bong" | "bing" | ... (and not just string), then you can express a type which is itself the union of Column<T, K> for each key K in keyof T.

I usually do this via immediately indexing (lookup) into a mapped type:

type SomeColumn<T extends RowData> = {
  [K in keyof T]-?: Column<T, K>
}[keyof T]

but you can also do it via distributive conditional types:

type SomeColumn<T extends RowData> = keyof T extends infer K ?
  K extends keyof T ? Column<T, K> : never : never;

Either way, your RowsAndColumns type would then use SomeColumn instead of Column:

type RowsAndColumns<T extends RowData> = {
  rows: Array<T>;
  columns: Array<SomeColumn<T>>;
}

And this makes your desired use case work as expected without compile errors:

myFn({
  rows: [
    {
      id: "foo",
      bar: "bar",
      bing: "bing",
      bong: {
        a: 99,
        b: "aaa"
      }
    }
  ],
  columns: [
    {
      key: "bar",
      action: (value, rowData) => {
        console.log(value);
      }
    },
    {
      key: "bong",
      action: (value, rowData) => {
        console.log(value.a);
      }
    },
  ]
});

Playground link to code

like image 69
jcalz Avatar answered Oct 25 '22 00:10

jcalz