skip to Main Content

I’ve got DataGrid component in my React project. I want it to be type safe, so I made generic type T for data and K as keyof T for column, but when I want to create columns Typescript doesn’t know the type of property instead it gives type as union.
Sample interfaces:

interface DataGridProps<T extends object> {
    data: T[];
    columns: Array<ColumnProps<T, keyof T & string>>;
    idField: keyof T & string;
}
interface ColumnProps<T, K extends keyof T> {
    field: K,
    sorter?: (a: T[K], b: T[K]) => number;
    formatter?: (value: T[K]) => string;
}

Here is playground with problem:
TS Playground

I tried:

Pass columns as list inline (not working)

const gridProps: DataGridProps<Person> = {
  data: [{id: 1, name: "Test", active: true}],
  columns: [{
    field: "id",
    sorter: (a: number, b: number) => 1,
  }],
  idField: "id"
}

Create list of columns (not working)

const columns: ColumnProps<Person, keyof Person>[] =
  [{field: "id", sorter: (a: number, b: number) => 1}]

Creating individual column with generic works, but is there any other solution that not require that much code?

const column1 : ColumnProps<Person, "id"> = {
  field: "id",
  sorter: (a: number, b: number) => 1,
}

2

Answers


  1. The problem is with the signature of the ColumnProps.sorter method’s signature during the assignment.

    Fix:

    const gridProps: DataGridProps<Person> = {
      data: [{id: 1, name: "Test", active: true}],
      columns: [{
        field: "id",
        // Use Person[keyof Person] because that is the correct signature.
        sorter: (a: Person[keyof Person], b: Person[keyof Person]) => 1,
      }],
      idField: "id"
    }
    
    const columns: ColumnProps<Person, keyof Person>[] =
      [{field: "id", sorter: (a: Person[keyof Person], b: Person[keyof Person]) => 1}]
    
    const column1 : ColumnProps<Person, "id"> = {
      field: "id",
      // This doesn't need Person[keyof Person] because typescript
      // knows we are targeting "id" property of Person which stores a number.
      sorter: (a: number, b: number) => 1,
    }
    

    Explanation:
    The reason why your code doesn’t work is when you say keyof Person, what you’re really telling Typescript is that it can be any of the types defined in the person interface. In addition to that, when defining ColumnProps, you said that sorter takes arguments that are of type T[K], which is string | number | boolean.

    So in your case, it can be a number, string or boolean since you define your Person interface as

    interface Person{
      id: number,
      name: string,
      active: boolean,
    }
    

    But when you defined one instance, you said I will target the id key. So, Typescript can now look at the Person interface and understand – alright, since id is of number type, I’ll let you assign a number.

    So, if you wanted to handle only number types to be sorted, then this would be the way to go:

    interface ColumnProps<T, K extends keyof T> {
        field: K,
        sorter?: (a: number, b: number) => number;
        formatter?: (value: T[K]) => string;
    }
    

    But since you want a generic sorter method, you would have to be consistent with the signature when using the type for which you defined the generic. Hence the fix I wrote at the start of the answer.

    Login or Signup to reply.
  2. You are actually really close to the desired solution. The problem is in ColumnProps with the K parameter. When you use ColumnProps in DataGridProps you pass K as keyof T & string which is resulted in a union, thus in the sorter T[K] is resolved to all possible values.

    Example:

    type Obj = {
      a: string;
      b: number;
      c: boolean;
    };
    
    type Func<Obj, K extends keyof Obj> = (value: Obj[K]) => null;
    
    // (value: string | number | boolean) => null
    type Result = Func<Obj, keyof Obj>;
    

    Since, we would expect to get:

    type Result = ((value: string) => null) | ((value: number) => null) | ((value: boolean) => null)
    

    this method doesn’t work and the solution is distributive conditional types. Basically, we need to check if K extends something to distribute it, but we must be sure that this condition will be always true. Possible ones are K extends K or K extend any, .etc.

    Testing:

    type Func<Obj, K extends keyof Obj> = K extends K
      ? (value: Obj[K]) => null
      : never;
    
    // ((value: string) => null) | ((value: number) => null) | ((value: boolean) => null)
    type Result = Func<Obj, keyof Obj>;
    

    Looks great! Now, let’s apply the same logic to ColumnProps:

    type ColumnProps<T, K extends keyof T> = K extends K
      ? {
          field: K;
          sorter?: (a: T[K], b: T[K]) => number;
          formatter?: (value: T[K]) => string;
        }
      : never;
    

    Usage:

    const columns: ColumnProps<Person, keyof Person>[] = [
      { field: 'id', sorter: (a, b) => a + b}, // a and b are inferred as numbers
    ];
    

    playground

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search