9

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

2
  • Can't check it right now, but will key: "bar" as const instead of just key: "bar" help? Commented Nov 9, 2020 at 2:34
  • @Cerberus nope - I tried that :) Commented Nov 9, 2020 at 3:58

1 Answer 1

12

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

Sign up to request clarification or add additional context in comments.

4 Comments

Thank you so much! What is that -? syntax - I have never seen that.
It removes the optional modifier from properties on the mapped type (otherwise you get weird undefineds in the value types); see this documentation
commenting to say this does not work as of TS 4.8.4
@AbeCaymo I just looked and I don't see any obvious problem; could you elaborate on what, specifically, "does not work"?

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.