Search code examples
typescriptdynamicinterface

TypeScript Interface dynamic value type


It's possible to declare a interface like this;

interface TableColumn<T> {
fieldName: key of T
render: (value: **value type of the fieldName** ) => void

}

I want to make a type related to keyof T. this is what I want to do:

type Person {
  name: string;
  age: number;
}

interface TableColumn<T> {
  fieldName: keyof T;
  render: (value: /* type of keyof T */) => unknown;
}

//eaxample: 

const NameColumn: TableColumn<Person> = {
  fieldName: 'name', // keyof T is Person[name]
  render: (value: ??) // ==> value must be a string type, becasuse Person.name is a string type
}


const AgeColumn: TableColumn<Person> = {
  fieldName: 'age', // keyof T is Person[age]
  render: (value: ??) // ==> value must be number type, becasuse Person.age is a number type
}

Solution

  • If we just use the following code:

    type TableColumn<T, K extends keyof T = keyof T> = {
      fieldName: K;
      render: (value: T[K]) => unknown;
    };
    

    The result won't be the expected one unfortunately, because the value in the render function will have the type of a union of all possible types in T. This can be fixed by using the conditional type distribution. When a distribution is applied to a union the code after the condition is applied to each member of the union separately:

    type FirstToArray<T> = T[]
    type First = FirstToArray<string | number> // (string | number)[]
    
    type SecondToArray<T> = T extends any ? T[] : never
    type Second = SecondToArray<string | number> // string[] | number[]
    

    Let's apply this logic to our type:

    type TableColumn<T, K extends keyof T = keyof T> = K extends K ? {
      fieldName: K;
      render: (value: T[K]) => unknown;
    } : never
    

    Testing:

    type ColumnsMap = {
      field1: string;
      field2: number;
    };
    
    const a: TableColumn<ColumnsMap> = {
      fieldName: 'field1',
      render: (value) => '' // value: string
    }
    

    playground