Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Automatically infer second generic argument from first one

Tags:

typescript

I have the following interface to define define a column of a table:

export interface IColumnDefinition<TRow, TField extends keyof TRow> {
    field: TField;
    label?: string;
    formatter?: (value: TRow[TField], row: TRow) => string;
}

Now what I want is to only provide the type of the rows (TRow), and let TypeScript automatically infer the type of the field (TField) based on the value in the field property.

Now assume I have the following interface for my rows:

interface User {
    name: string;
    birthDate: number;
}

What I tried is the following:

const birthDateColumnDefinition: IColumnDefinition<User> = {
    field: 'birthDate',
    formatter: value => new Date(value).toDateString(),
}

Which gives me the following error:

Generic type 'IColumnDefinition<TRow, TField extends keyof TRow>' requires 2 type argument(s).

What I also tried is to use a function to create the definition, hoping that the type can be inferred from an argument:

function createColumnDefinition<TField extends keyof TRow>(
    field: TField,
    columnDef: Partial<IColumnDefinition<TRow, TField>>): IColumnDefinition<TRow, TField>
{
    return {
        ...columnDef,
        field,
    };
}

const birthDateColumnDefinition = createColumnDefinition<User>('birthDate', {
    formatter: (value, row) => new Date(value).toDateString(),
});

This gives me a comparable error:

Excepted 2 type arguments, but got 1.

If however I would also include the row as an argument, and remove all the generic arguments all together, it works fine:

function createColumnDefinition<TRow, TField extends keyof TRow>(
    row: TRow,
    field: TField,
    columnDef: Partial<IColumnDefinition<TRow, TField>>): IColumnDefinition<TRow, TField>
{
    return {
        ...columnDef,
        field,
    };
}

const user: User = {
    name: 'John Doe',
    birthDate: 549064800000,
};

const birthDateColumnDefinition = createColumnDefinition(user, 'birthDate', {
    formatter: (value, row) => new Date(value).toDateString(),
});

This does work, but this isn't an option since I don't actually have a row when defining the columns.

So is there any way to make this work (preferably by using an interface and not a function)?

like image 810
sroes Avatar asked Aug 15 '18 12:08

sroes


2 Answers

Now what I want is to only provide the type of the rows (TRow), and let TypeScript automatically infer the type of the field (TField) based on the value in the field property.

This looks very similar to partial type inference which is not currently supported in typescript, and it's not clear if it will ever be supported for this particular use case.

But there is one thing you can do - you can have one function that accepts explicit type parameter for TRow, and returns another function that will infer TField from its parameter. The syntax is a bit unwieldy, but it works:

function columnDefinition<TRow>(): 
   <TField extends keyof TRow>(def: IColumnDefinition<TRow, TField>) => 
        IColumnDefinition<TRow, TField> {
    return  <TField extends keyof TRow>(def: IColumnDefinition<TRow, TField>) => def
}

export interface IColumnDefinition<TRow, TField extends keyof TRow> {
    field: TField;
    label?: string;
    formatter?: (value: TRow[TField], row: TRow) => string;
}

interface User {
    name: string;
    birthDate: number;
}

To use it, you call columnDefinition<User>() without any parameters, and then immediately call returned function with column definition object as parameter:

const birthDateColumnDefinition = columnDefinition<User>()({
    field: 'birthDate',
    formatter: value => new Date(value).toDateString(), 
            // value inferred as (parameter) value: number
});
like image 75
artem Avatar answered Nov 11 '22 06:11

artem


I ended up using artem's answer to write a column definition builder class:

export class ColumnDefinitionBuilder<TRow> {

  private _columns: Array<IColumnDefinitionWithField<TRow, any>> = [];

  public define<TValue>(field: ((row: TRow) => TValue) | string, def: IColumnDefinition<TRow, TValue>)
    : ColumnDefinitionBuilder<TRow> {
    this._columns.push({...def, field});
    return this;
  }

  public get columns() {
    return this._columns;
  }
}

Which allows for the following syntax to define the columns:

const userColumnDefinitions = new ColumnDefinitionBuilder<User>()
    .define(x => x.birthDate, {
        label: 'Birth date',
        formatter: x => new Date(x).toDateString(),
    })
    .define(x => x.name, {
        label: 'Name',
    })
    .columns;
like image 42
sroes Avatar answered Nov 11 '22 07:11

sroes