【问题标题】:Why can TypeScript's compiler not analyze an array of types?为什么 TypeScript 的编译器不能分析类型数组?
【发布时间】:2020-12-28 15:44:14
【问题描述】:

设置:给定一些打字稿代码,例如:

type ObjectList = {
  [index: string]: string;
};

function makeList(input: ObjectList | string | number): string[] {
  if (typeof input === "string" || typeof input === "number") {
    return [String(input)];
  }
  const arr = [];
  for (const x in input) {
    arr.push(String(input[x]));
  }
  return arr;
}

// Turns a number into a string array
console.log(makeList(123));

// Turns a string into an array
console.log(makeList("hello"));

// Turns an object dictionary into an array
console.log(makeList({ a: "A", b: "B" }));

好的,很简单,而且效果很好。对类型安全至关重要的是这一行:

if (typeof input === "string" || typeof input === "number") {

它确保for in 将只在ObjectList 上运行,TypeScript 编译器很好地为我们嗅出它!这里的缺点是那行代码非常冗长,尤其是如果您要添加更多类型,或者我们应该能够做到这一点:

if (['string', 'number'].includes(typeof input)) {

但是,这将导致 TypeScript 编译器错误,基本上表明 for loop 不确定 stringnumber 类型无法访问它。

'for...in' 语句的右侧必须是 'any' 类型、对象类型或类型参数,但这里有类型 'string |号码 |对象列表

这是一个运行示例:

https://codesandbox.io/s/typescript-arrayincludestypeof-var-dblkn?file=/src/index.ts

问题:为了更好地理解编译器——我对编译器为何表现出这种行为感兴趣。 [].includes() 与编译器不兼容怎么办?静态分析是不是太深了?有没有我没有想到的运行时注意事项?在比较中使用时,TS 中的 typeof 运算符是否绑定了一些额外的“魔法”?

【问题讨论】:

  • "是不是静态分析太深了?"是的。如果打字稿必须检查每个数组的值并记住对这些数组进行操作的每个函数的逻辑,那会慢很多。
  • 我很好奇 - 添加any 类型转换能解决问题吗?就像,这行得通吗? if (['string', 'number'].includes(typeof (input as any))) {
  • 当你有一个 if 语句来缩小类型时,Typescript 会进行分析,但它只在非常基本的级别上执行此操作。如果您需要更复杂的东西,您可以定义自己的函数来缩小类型 - 此功能称为类型保护,请参阅:typescriptlang.org/docs/handbook/…
  • 如果你想为你的代码保证类型安全,不要改变你的数组。 TS不喜欢

标签: typescript


【解决方案1】:

cmets 中提到或暗示了这一点,但为了完整起见,我想扩展为一个答案。


当您直接检查typeof input === "string" 时,编译器会将其视为类型保护,这对input 的表观类型有影响。而在检查之前,input 只知道是它声明的类型(例如,ObjectList | string | number),在检查之后,编译器可以给它一个更窄的在检查返回true(例如string)时才能到达的代码区域中输入明显,并且在检查返回时才能到达@987654333 时才能到达的代码区域中的另一种明显类型@(例如,ObjectList | number)。这称为control flow analysis

通过模拟代码在所有可能输入上的行为并给input 它可以采用的所有可能值的联合,这种控制流分析不起作用。它也不能通过直觉和智力起作用;当且仅当某些变量比其声明的类型更窄时,编译器无法简单地“理解”某些代码块是可访问的。取而代之的是,已经实现了一系列特定的启发式,对应于已知用作类型保护的不同可识别和常见编码模式。

typeof someVariable === "string" 视为类型保护就是这样一种启发式方法the typeof type guard

["string"].includes(typeof someVariable)被编译器视为类型保护;没有人实现过这样的事情。


关于类似问题/建议的有趣讨论可以在 microsoft/TypeScript#36275 找到:为什么 TypeScript 不将 Array.includes()Array.indexOf() 视为类型保护?

正如@RyanCavanaugh(微软 TypeScript 团队的开发负责人)所说的in a comment

[缺少类型保护] 是缺少实现某个功能的行为,这会导致它的行为方式与 OP 提议的方式相同。现有的收缩机制都不适用于这里;我们谈论的可能是一千行新代码以正确根据这些方法检测和缩小数组,以及每个程序支付的性能损失,因为控制流图必须更细化设置由任何方法调用引起的可能缩小,以及由于人们期望这些函数的非方法版本也具有相同行为而引起的错误跟踪和混淆跟踪。

我想这个案子也会这样说;在编译器中实现这种类型保护需要大量的工作,让每个人的编译器都变慢,并引入更多的复杂性和可能的​​错误,所有这些都是为了支持一个相对不常见的用例。


不过,幸运的是,TypeScript 确实让开发人员能够通过 user-defined type guard functions 编写自己的自定义类型保护。您需要将您的预期类型保护重构为一个作用于其参数之一的boolean-returning 函数,这意味着您仍然无法让["string"].includes(typeof input) 开箱即用,但您可以在尽量不要靠近。

这是一种可能的实现方式:

interface TypeofMap {
  string: string;
  number: number;
  bigint: bigint;
  boolean: boolean;
  symbol: symbol;
  undefined: undefined;
  object: object | null;
  function: Function
}
function typeofIncludes<K extends keyof TypeofMap>(typeofs: K[], val: any): val is TypeofMap[K];
function typeofIncludes(typeofs: Array<keyof TypeofMap>, val: any) {
  return typeofs.includes(typeof val);
}

我定义了一个TypeofMap 接口,其唯一目的是表示JS typeof 运算符返回的字符串值与它们所代表的TypeScript 类型之间的映射。它并不完美;没有像 "object" 字符串对应的“非函数对象”这样的 TypeScript 类型,我没有尝试模拟一个,而是使用 object

然后,typeofIncludes() 函数接受 typeof-outputtable 字符串的数组 typeofs 和值 val,如果 val 是相关之一,则返回 booleantrue类型。

你可以在这里测试一下:

function makeList(input: ObjectList | string | number): string[] {
  if (typeofIncludes(["string", "number"], input)) {
    return [String(input)];
  }
  const arr = [];
  for (const x in input) {
    arr.push(String(input[x]));
  }
  return arr;
}

这行得通。在if (typeofIncludes(["string", "number"], input)) 的“then”子句中,input 被缩小为string | number,而在“else”子句中,它被缩小为ObjectList


Playground link to code

【讨论】:

    【解决方案2】:

    与其问你的代码是什么“不兼容”,不如问是什么让像typeof input === 'number'这样的检查工作更有启发性,然后我们可以看到同样的机制不适用于includes条件。

    引用Typescript manual:

    typeof 类型保护以两种不同的形式被识别:typeof v === "typename"typeof v !== "typename",其中 "typename" 可以是 typeof 运算符的返回值之一("undefined""number""string""boolean""bigint""symbol""object""function")。

    所以条件typeof input === 'number' 允许控制流类型缩小,因为该语言专门针对这种特定形式的条件进行类型缩小。 Typescript 文档列出了允许类型缩小的所有特定形式的条件:

    • 用户定义的类型保护,即返回类型为 arg is Type 的函数。
    • keyName in obj
    • typeof obj === 'typename'
    • obj instanceof ClassName
    • 通过检查相等性或真实性从联合中消除 null 和/或 undefined

    就是这样。所以['string', 'number'].includes(typeof input) 不做类型缩小的原因仅仅是因为这个条件不是编译器识别的特定形式之一。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2021-02-19
      • 2014-01-18
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多