【译】TypeScript:catch子句中的类型收窄

818 阅读4分钟

译者的话

很久以来,我一直觉得 JavaScript 中处理错误很别扭,特别是在尝试给错误标注TypeScript 类型的时候,总觉得怎么标注都不合适。我就是在尝试给 catch 子句的 e 标注类型时发现 vscode 给我报了个 ts(1196) 的错误,然后通过搜索引擎找到了这篇文章,看完后醍醐灌顶。另外axios官方文档推荐的错误处理方式也跟下文中提及的不谋而合。下面是正文。


如果你是 Java、C++ 或 C# 等语言的开发者,你可能习惯于通过抛出异常来进行错误处理。随后,用一连串的 catch 子句捕获它们。可以说有很多更好的方法来处理错误,但这种方式已经存在了很长时间,考虑到历史和影响,它也进入了JavaScript。

因此,这是在 JavaScript 和 TypeScript 中进行错误处理的有效方法。但当你尝试遵循与其他编程语言相同的流程,并在 catch 子句中注释错误的类型时:

try {
  // something with Axios, for example
} catch(e: AxiosError) {
//         ^^^^^^^^^^ Error 1196 💥
}

TypeScript 将抛出 TS1196 的错误:Catch 子句变量类型注释必须为 "any" 或 "unknown" (若已指定)。

抛出这样的错误有下面几个原因:

1. throw语句可以抛出任何类型

JavaScript 允许你 throw 任何表达式。当然了,你可以抛出“异常”(或者叫错误吧,JavaScript 中我们通常是这样称呼的),但也可以抛出任何其他值:

throw "What a weird error"; // 👍
throw 404; // 👍
throw new Error("What a weird error"); // 👍

由于可以抛出任何有效值,因此要捕获的可能值已经比通常的Error的子类型要广泛得多了。

2. JavaScript 中只有一个 catch 子句

JavaScript 中每个 try 语句只有一个 catch 子句。很久以前就有过关于多重 catch 子句甚至是条件表达的提案,但从未付诸实施。可以参考《JavaScript权威指南》来验证这一点——但是先别急——再看看JavaScript 1.5——什么鬼?!?(译者注:这里JavaScript1.5中多重catch子句的提案仍然是待讨论状态)

相反,你应该像下面这样使用 catch 子句并使用 instanceoftypeof 来做类型检查(来源):

try {
  myroutine(); // There's a couple of errors thrown here
} catch (e) {
  if (e instanceof TypeError) {
    // A TypeError
  } else if (e instanceof RangeError) {
    // Handle the RangeError
  } else if (e instanceof EvalError) {
    // you guessed it: EvalError
  } else if (typeof e === "string") {
    // The error is a string
  } else if (axios.isAxiosError(e)) {
    // axios does an error check for us!
  } else {
    // everything else  
    logMyErrors(e);
  }
}

注意:上面的示例也是 TypeScript 中 catch 子句类型收窄的唯一正确方法。

由于可以抛出所有可能的值,并且每个 try 语句只有一个 catch 子句来处理它们,因此 e 的类型范围非常广泛。

3. 任何异常都可能发生

既然已经知道可能发生的每个错误,那么是不是用一个合适的联合类型就可以很好的注解所有有可能的“可被抛出”的类型了呢?但实际上,我们没有办法知道异常将具有哪些类型。

除了所有用户定义的异常和错误之外,当内存出现问题、遇到类型不匹配或某个函数未定义时,系统都有可能会抛出错误。一个简单的函数调用都可能会超出您的调用堆栈并导致臭名昭著的栈溢出。

广泛的可能值集、单个 catch 子句,以及发生的错误的不确定性,导致了 e 只允许两种可能类型:anyunknown

Promise的reject态又该怎么处理呢?

如果你 reject 了一个 Promise,情况也是如此。 TypeScript 唯一允许您指定的是 fulfilled 状态的 Promise 的类型。rejection 可能是因为你在代码中确实这样做了,也可能是因为系统错误:

const somePromise = () => new Promise((fulfil, reject) => {
  if (someConditionIsValid()) {
    fulfil(42);
  } else {
    reject("Oh no!");
  }
});

somePromise()
  .then(val => console.log(val)) // val is number
  .catch(e => {
    console.log(e) // e can be anything, really.
  })

如果你在 asnyc/await 流中调用相同的 promise,情况会变得更清晰:

try {
  const z = await somePromise(); // z is number
} catch(e) {
  // same thing, e can be anything!
}

结论

如果你在使用的其它编程语言有类似的特性,那么 JavaScript 和 TypeScript 中的错误处理方式可能会让你感觉“一见如故”,但这很可能让你掉进陷阱。请注意差异,并相信 TypeScript 团队和类型检查器会为您提供正确的控制流程,以确保你的错误得到足够好的处理。