【问题标题】:Automatically infer second generic argument from first one从第一个参数自动推断第二个通用参数
【发布时间】:2019-01-22 07:55:37
【问题描述】:

我有如下接口来定义表的一列:

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

现在我想要的是只提供行的类型 (TRow),并让 TypeScript 根据 field 属性中的值自动推断字段的类型 (TField)。

现在假设我的行有以下界面:

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

我尝试的是以下内容:

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

这给了我以下错误:

通用类型 'IColumnDefinition' 需要 2 个类型参数。

我也尝试过使用函数来创建定义,希望可以从参数中推断出类型:

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(),
});

这给了我一个类似的错误:

排除了 2 个类型参数,但得到了 1 个。

但是,如果我还将该行作为参数包含在内,并一起删除所有通用参数,它可以正常工作:

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(),
});

这确实有效,但这不是一个选项,因为我在定义列时实际上没有一行。

那么有什么方法可以使这项工作(最好是使用接口而不是函数)?

【问题讨论】:

  • 你可以在这两个地方用keyof TRow替换TField,然后你只需要提供TRow。但是,您将丢失 row[field] 的特定类型。
  • 是的,但我想在列定义的其他属性中使用TField。以formatter的第一个参数为例。

标签: typescript


【解决方案1】:

现在我想要的是只提供行的类型 (TRow),并且 让 TypeScript 自动推断字段的类型(TField) 基于字段属性中的值。

这看起来与打字稿当前不支持的partial type inference 非常相似,而且尚不清楚它是否会在此特定用例中得到支持。

但是您可以做一件事 - 您可以让一个函数接受 TRow 的显式类型参数,并返回另一个函数,该函数将从其参数推断 TField。语法有点笨拙,但很有效:

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;
}

要使用它,你可以不带任何参数调用columnDefinition&lt;User&gt;(),然后立即以列定义对象为参数调用返回函数:

const birthDateColumnDefinition = columnDefinition<User>()({
    field: 'birthDate',
    formatter: value => new Date(value).toDateString(), 
            // value inferred as (parameter) value: number
});

【讨论】:

  • 这是一个聪明的技巧,但并不是我真正希望的解决方案 :-) 希望 TypeScript 有一天能够支持部分类型推断。
【解决方案2】:

我最终使用 artem 的答案来编写列定义构建器类:

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;
  }
}

允许使用以下语法来定义列:

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;

【讨论】:

    猜你喜欢
    • 2020-09-24
    • 1970-01-01
    • 2021-12-20
    • 1970-01-01
    • 2021-11-11
    • 2019-11-25
    • 2018-03-22
    • 1970-01-01
    • 2011-10-11
    相关资源
    最近更新 更多