首页 > 解决方案 > 为什么 TypeScript 显示无法执行路径的错误?

问题描述

我有这个示例代码:

function func(a?: number, b?: number, c?: number) {
  let allGood: boolean = true;
  
  if(!a) {
    console.log('a is unacceptable, so do something with a')
    allGood = false;
  }

  if(!b) {
    console.log('b is unacceptable, so do something with b')
    allGood = false;
  }

  if(!c) {
    console.log('a is unacceptable, so do something with c')
    allGood = false;
  }

  if(allGood) {
    console.log(a + b + c) // Error: "Object is possibly undefined" showing for a, b and c
  } else {
    console.log('oh no!')
  }
}

你可以看到,最终只有allGood当 all且不为假(包括)时才为真。但是在最后一个条件中,TypeScript 抱怨每个,和可能是未定义的。在不重新检查真实性的情况下解决此问题的最佳方法是什么,并且因为在此条件之前已经检查过它们。abcundefinedabcabc

标签: typescript

解决方案


简短的回答:控制流缩小本质上是启发式的,不考虑变量之间的这种相关性。microsoft/TypeScript#20497存在一个未解决的问题,请求支持所谓的“分支标志”,例如您的allGood. 但实施起来可能太复杂了。


长版:

TypeScript 使用控制流分析来缩小代码块中变量和属性的明显类型,在这些类型保护或赋值发生后,它可以跟踪代码块。因此,例如,在以下示例代码中,编译器可以看到真实性检查a会将其范围从 缩小number | undefinednumber

if (!a) { } else {
  a + 1; // okay
}

但是在语句的真假分支if都完成后,如果来自多个路径的控制流再次连接起来,编译器会将缩小的类型重新连接到每个分支的任何缩小的联合中:

let x: number | string | undefined = Math.random() < 0.5 ? 123 : undefined;
// x is number | undefined
if (typeof x === "undefined") {      
  // x is undefined
  x = "hello"; // x is string
} else {
  x += 789; // x is number
}
// x is string | number
x.valueOf(); // okay

但请注意,这些变窄/变宽通常针对不同的变量独立发生:

let someBoolean: boolean;
let x: number | string | undefined = Math.random() < 0.5 ? 123 : undefined;
// x is number | undefined
if (typeof x === "undefined") {      
  // x is undefined
  x = "hello"; // x is string
  someBoolean = true;
} else {
  x += 789; // x is number
  someBoolean = false;
}
// x is string | number
x.valueOf(); // okay
someBoolean; // boolean

在此块的末尾,someBooleanis boolean(相当于 union true | false)和xis string | number。现在事实证明someBooleanx相互关联的;someBoolean不能是truewhilex是 a numbersomeBoolean也不能是falsewhilex是 a string。但是编译器不会跟踪这种相关性。它将它们视为不相关独立的联合类型,因此假设这种不可能的情况是可能的。

我打开了microsoft/TypeScript#30581来强调这是人们遇到的一个痛点,但是没有明显的解决方案可以工作。

为了自动和一般地跟踪相关性,编译器必须开始进行计算,以模拟它考虑的每个联合类型变量或属性的每一种可能的缩小。对于每一个额外的变量或属性,这会使编译器的工作量乘以某个因子。因此编译时间会在变量和属性的数量上呈指数增长。所以这不可能发生。

正如封闭的microsoft/TypeScript#25051中所建议的那样,可以想象一种要求编译器考虑手动指定的变量或属性集的方法,以便您只在对您而言值得时付出性能损失。该建议因多种原因而被关闭,包括将函数体包装在类似type switch (a, b, c) { ... }.

并且在microsoft/TypeScript#20497有一个特定的请求,以支持allGood跟踪特定条件的“分支标志” 。但同样,没有明显的方法来实现它并保持合理的编译器性能。


结语:

您将不得不解决这个问题;通过使用类型断言a告诉编译器您对,b和的类型了解c得比它多:

if (allGood) {
  console.log(a! + b! + c!) // assertions
} else {
  console.log('oh no!')
}

(这里我使用了非空断言操作符!作为更简洁的版本(a as number) + (b as number) + (c as number)

或者通过重构为不依赖于此类相关变量的版本,例如(诚然奇怪):

function func(a?: number, b?: number, c?: number) {

  let good = (a: number) => (b: number) => (c: number) => () => console.log(a + b + c);

  let aGood;
  if (!a) {
    console.log('a is unacceptable, so do something with a')
  } else {
    aGood = good?.(a);
  }

  let abGood;
  if (!b) {
    console.log('b is unacceptable, so do something with b')
  } else {
    abGood = aGood?.(b)
  }

  let abcGood;
  if (!c) {
    console.log('c is unacceptable, so do something with c')
  } else {
    abcGood = abGood?.(c)
  }

  if (abcGood) {
    abcGood();
  } else {
    console.log('oh no!')
  }
}

func(1, 2, 3) // 6;
func(1) // b unacceptable, c unacceptable, oh no!

显然,在这种情况下,断言不那么突兀,所以这就是我的建议。

Playground 代码链接


推荐阅读