如果你有使用过像 Java、C++、或者 C# 这类编程语言的经验,你通常会透过抛出异常来做错误处理,然后使用一连串的 catch 语句来捕获它们。虽然已经证明有其他更好的方式来处理错误,但因为抛异常这种方式悠久的历史以及对编程习惯的广泛影响,我们依旧会在 JavaScript 当中继续使用它。

当然,这种方式的错误处理在 JavaScript 和 TypeScript 中是行之有效的,但如果你像下面这个例子一样照搬在其他编程语言的使用习惯,在 catch 语句里定义错误类型的话。

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

你将收获一个 TS1196 错误:
Catch clause variable type annotation must be ‘any’ or ‘unknown’ if specified.

catch 语句变量类型只能声明为 any 或者 unknown.

之所以这样,有下面几条理由:

1 . 你可以抛出任何类型

在 JavaScript 中, 你可以抛出任何的表达式。通常来说,我们会抛出 “异常” ( 在 JavaScript 中我们叫做 错误 Error ),但也不能排除我们会抛出其他任何类型的可能性:

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

正因为任何合法的类型都能够被抛出,那 catch 语句能捕获到的类型也就不仅限于我们过去理解的错误或其子类型。

2 . JavaScript 中只能使用一个 catch 语句。尽管在很早之前就有了关于多路捕获甚至是在 catch 语句中加入条件表达式的提案,但直到今天这些你还不能使用这些新特性。

了解更多 JavaScript - the definitive guide

作为替代方案,我们可以通过在一个 catch 语句内部使用 instanceof 或者 typeof 类型检查来实现。

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 中进行 收敛异常捕获 的最佳实践。

正是由于能够抛出任何可能的值,同时我们对一个 try 语句只能使用有且只有一个 catch,那么这个 e 的类型范围如此的宽泛,就一点都不奇怪了。

3 . 无法预测的事情总在发生

那么你可能又要问了,在上面的例子里面,既然你知道所有可能发生的异常,我们就不能定义一个恰到好处的联合类型来表达它吗?
理论上是可以的,不过在实践中我们发现,根本无法穷举所有的异常类型。

除了一些用户自己定义的错误和异常,变量的类型不匹配,或者你的一个函数未定义,都会让系统抛出一个内存错误。更不用说,一个简单的方法超出了调用栈的最大数量,都会引起臭名昭著的 stack overflow

异常的类型范围如此之宽广,而 catch 语句又只有一个,还要应对各种不确定的意外,就造成了这个 e 只能是 anyunknown 的局面。

Promise 的拒绝行为有什么不一样呢?

正确答案是:一毛一样。
typescript 只允许你去定义完成态(fullfilled)的值类型,对于拒绝行为(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 模式的话,它的表现会更符合上面的表达:

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

写在最后

很显然,对于从其他编程语言转移过来的开发者来讲,JavaScript 和 typescript 的错误处理并不十分友好。

我们能够做的,就是充分了解此间的差异,并坚信在不久的将来, typescript 团队给我们的类型检查能力,会让我们的错误处理足够的好。

原文链接:https://fettblog.eu/typescript-typing-catch-clauses/