4

我正在尝试向我的开源库Angular-Slickgrid添加更多通用类型,这是一个数据网格库。

我为 Column Definitions 定义了一个接口,它可以有多个 Type Column,我正在向这个Column接口添加一个新的 Generic Type ,我想在formatter它本身是 Type 的选项中使用这个新的 Type Formatter,如下所示。新的通用类型基本上是告诉格式化程序,它可能是项目对象的类型。

export interface Column<T = any> {
  field: string;

  // oops that was a typo
  // formatter?: Formatter: T; 

  // correct code is actually this
  formatter?: Formatter<T>;
}

定义Formatter如下

// add Generic Type for the item object
export declare type Formatter<T = any> = (row: number, cell: number, value: any, columnDef: Column, item: T) => string;

现在我正在尝试通过创建一个ReportItem传递给的接口来使用新的泛型,columnDefinitions以便我可以(希望)ReportItem在我的Formatter

interface ReportItem {
  title: string;
  duration: number;
  cost: number;
  percentComplete: number;
  start: Date;
  finish: Date;
  effortDriven: boolean;
}

const customEditableInputFormatter: Formatter = <T = any>(row: number, cell: number, value: any, columnDef: Column, item: T) => {
  // I want the `item` to be of Type ReportItem but it shows as Type any
  // item.title 
};

export class MySample {
  columnDefinitions: Column<ReportItem>[];

  initializeGrid() {
    this.columnDefinitions = [
      {
        id: 'title', name: 'Title', field: 'title', sortable: true, type: FieldType.string,
        formatter: customEditableInputFormatter,
      }
    ];
  }
}

如果我将鼠标悬停在该formatter属性上,我会Formatter<ReportItem>通过智能感知(在 VSCode 中)看到正确的内容。 格式化程序属性智能感知

但是如果我将鼠标悬停在自定义格式化程序本身上,我会得到Formatter<any>

自定义格式化程序智能感知

所以我的问题是如何对其进行编码,以便它在我的外部格式化程序中真正采用正确的类型ReportItem,我希望我的外部格式化程序是 TypeFormatter<ReportItem>而不是Formatter<any>. 我开始学习泛型,我发现它非常强大,但仍有一些方法可以学习它们。

感谢您提供的任何帮助。

编辑 1

我还尝试在自定义格式化程序 ( ) 上替换<T = any>by并在使用时添加 a ,但它仍然显示为<T>const customEditableInputFormatter: Formatter = <T>...customEditableInputFormatter as Formatter<ReportItem>Tany

格式化程序属性 2

自定义格式化程序类型 2

4

2 回答 2

2

概述

问题似乎主要是使用正确的泛型语法问题,这看起来可能反映了您在学习新概念时的轻微误解。

我已将您的代码重写为下面的工作形式。密切注意泛型参数相对于冒号和赋值运算符出现的位置,因为它会使错误变得非常微妙。TypeScript 编译器没有帮助,因为提供的声明仍然是有效的语法(除了item.title非工作版本中的 squiggles),它只是不是您想要实现的正确语法。TypeScript 不了解您要做什么,只了解您所做的 :-)


Column<T>

首先,让我们修复该Column<T>定义,使其正确键入formatter属性。这可能只是一个错字,但我想提一下。(我还添加了您的方法中使用的缺失属性MySample.initializeGrid()):

export interface Column<T = any> {
  field: string;
  id: number | string;
  name?: string;
  sortable?: boolean;
  type?: FieldType;
  formatter?: Formatter<T>; // originally formatter?: Formatter: T;
}

Formatter<T>

现在,这是您的通用Formatter<T>定义,没关系:

export declare type Formatter<T = any> = (
  row: number, cell: number, value: any, columnDef: Column, item: T
) => string;

但是,根据您使用 参数化属性的Column<T>定义,您可能希望使用与匹配的相同通用参数指定 arg ,如下所示:formatterTcolumnDefTitem

export declare type Formatter<T = any> = (
  row: number, cell: number, value: any, columnDef: Column<T>, item: T
) => string;

但这是您必须决定的额外细节。现在到主要问题...


customEditableInputFormatter解决方案 (TLDR)

首先,在深入研究一堆 TypeScript 语言语法细节之前,让我先给出最简洁的 TLDR 单行工作版本。它利用 TypeScript 的类型推断,因此所有参数和返回值都是基于将其声明为的强类型Formatter<ReportItem>

const customEditableInputFormatter: Formatter<ReportItem> = (r, c, v, d, i) => i.title;

您可以写出完整的参数名称,指定它们的所有类型,指定返回类型,并在函数体周围添加花括号,但这对于强类型、智能感知等来说是绝对必要的。这里有一个稍微长一点的形式不依赖 IDE 来告诉读者类型是什么:

const customEditableInputFormatter: Formatter<ReportItem> = (
  row: number, cell: number, value: any, columnDef: Column<ReportItem>, item: ReportItem
) => {
  return item.title; // item is type `ReportItem`
}

如果您不关心确保签名匹配 aFormatter<T>以帮助在创建函数时避免错误,您甚至可以将其缩短为 this 而无需将其声明为Formatter<ReportItem>

const customEditableInputFormatter = (
  row: number, cell: number, value: any, columnDef: Column<ReportItem>, item: ReportItem
) => {
  return item.title; // item is type `ReportItem`
}

TypeScript 仍然允许稍后分配到Formatter<T>,但它容易出错,我不推荐它。


customEditableInputFormatter错误分析

因此,让我们更详细地查看您的代码......

这是您customEditableInputFormatter声明的原始版本:

const customEditableInputFormatter: Formatter = <T = any>(
  row: number, cell: number, value: any, columnDef: Column, item: T
) => {
  // I want the `item` to be of Type ReportItem but it shows as Type any
  // item.title // <-- oops, item is type `T` here, i.e. `any`
};

这个版本有几点需要注意:

  1. 在赋值运算符的左侧(第一个等号),您会看到您已将其类型声明为Formatter没有实际指定其类型参数T。因此,它默认为any,正如类型声明 ( <T = any>) 中所指定的那样,它永远不会是其他任何东西。
  2. 在赋值运算符的右侧,您有<T = any>右前括号的函数。这不是Formatter对左侧的类型进行参数化 - 这已在 #1 中指定。这是为您在右侧创建的新匿名泛型函数声明一个泛型参数!这是两个不同T的 s,我认为这在这里增加了混乱!该声明与对 a 的赋值兼容Formatter<any>,因此 TypeScript 不会抱怨。
  3. 令人惊讶的是,将 #2 更改为<T = ReportItem>仍然不能解决问题,即使将鼠标悬停item在函数体中表示它是 type ReportItem!这个怎么可能?这有点棘手,但这是因为函数体必须支持所有可能T的 s,而不仅仅是 whenT是 default ReportItem。TypeScript 无法知道任何随机数T都会有一个属性,因此即使我们知道在这种情况下它有一个title属性,它也不会让你使用它。item起初它是违反直觉的,但一旦你考虑一下它就会有道理。无论如何,声明它的更好方法是在上面的工作版本中,这不是问题。

一条建议

= any最后,如果可以的话,我建议删除所有默认类型参数。我认为它在通过阻止 TypeScript 更直接地提供帮助来掩盖错误方面发挥了重要作用。您提到向现有代码库添加类型,所以这可能使其更具挑战性。但是,删除它们允许语言服务标记默认类型模糊的问题。明智地使用它们会非常方便,但除非明确需要,否则我会跳过它们。


好的,这已经很多了,对于像这样的各种泛型问题,还有很多其他可能的事情要说。但是,我将在此结束,因为问题已得到解答。希望这对您有所帮助,并继续学习和使用 TypeScript 中的泛型!

于 2020-06-17T06:42:01.110 回答
1

让我们customEditableInputFormatter变成一个高阶函数:

const customEditableInputFormatter: <T>() => Formatter<T> = () => 
  <T>(row: number, cell: number, value: any, 
    columnDef: Column, item: T) => '';

上面的高阶函数返回另一个函数,该函数又返回一个字符串(我知道它非常没用,但它可以通过这样做显示我在 VSCode 上得到了什么):

在此处输入图像描述

在上图中,您可以看到嵌入在高阶函数的返回类型中的类型,而不仅仅是Formatter<any>.

这种方法显然使代码过于复杂,有利于 IDE 智能感知,因此 IMO,它不是在您的代码中使用的好选择。但也许它可以对问题有所了解,以帮助找到一个好的解决方案。

于 2020-06-11T02:54:14.480 回答