用详尽的类型检查改进TypeScript的错误处理

715 阅读6分钟

很少有程序能在完全隔离的情况下工作。即使开发者总是写出完美的代码,当代码与外部组件(如数据库、REST APIs,甚至是那个有一堆星星的时尚npm包)交互时,也很有可能遇到错误

一个负责任的开发者不会在服务器崩溃的时候坐以待毙,而是以防御性思维,为畸形请求的到来做好准备。在这篇文章中,我们将介绍TypeScript中几种最常见的错误处理方法。我们将学习每种方法的使用方法,看看每种方法可以如何改进,最后,提出一种更简洁的方法来管理错误

让我们开始吧!

注意:如果你不熟悉TypeScript,那么好消息来了!导致错误的条件和解决方案也适用于JavaScript。

流行的TypeScript错误处理方法

在我们深入研究之前,请记住,下面的列表绝不是详尽无遗的这里介绍的错误和解决方案都是基于我的主观经验,所以你的里程可能会有所不同!🏎

返回null

返回null是一种常见的方式,表示你的代码中的某些地方出了问题。当一个函数只有一种失败方式时,最好使用这种方法,然而,当一个函数有许多错误时,一些开发者会使用这种方法。

返回null会迫使你的代码中到处都是null检查,导致关于错误原因的具体信息被丢失。返回null是对错误的任意表示,所以如果你尝试返回0-1 、或false ,你会得到同样的结果。

在下面的代码块中,我们将写一个函数来检索关于某个城市的温度和湿度的数据。getWeather 函数通过两个函数externalTemperatureAPIexternalHumidityAPI 与两个外部 API 进行交互,并将结果汇总。

const getWeather = async (
  city: string
): Promise<{ temp: number; humidity: number } | null> => {
  const temp = await externalTemperatureAPI(city);
  if (!temp) {
    console.log(`Error fetching temperature for ${city}`);
    return null;
  }
  const humidity = await externalHumidityAPI(city);
  if (!humidity) {
    console.log(`Error fetching humidity for ${city}`);
    return null;
  }
  return { temp, humidity };
};
// ...
const weather = await getWeather("Berlin");
if (weather === null) console.log("getWeather() failed");

我们可以看到,在进入Berlin ,我们收到错误信息Error fetching temperature for ${city}Error fetching humidity for ${city}

我们的两个外部API函数都可能失败,所以getWeather 被迫为两个函数检查null。尽管检查null比完全不处理错误要好,但它迫使调用者做出一些猜测。如果一个函数被扩展到支持一个新的错误,调用者就不会知道,除非他们检查函数的内部。

假设externalTemperatureAPI ,当温度API返回HTTP code 500 ,表示一个内部服务器错误时,最初抛出一个空。如果我们扩展我们的函数,同时检查API响应的结构和类型的有效性(即检查响应是否属于type number ),那么调用者将不知道函数返回null是由于HTTP code 500 ,还是由于一个意外的API响应结构。

使用抛出自定义错误try/catch

创建自定义错误并抛出它们是比返回null更好的选择,因为我们可以实现错误粒度,这使得一个函数可以抛出不同的错误,并允许函数的调用者分别处理不同的错误。

然而,任何抛出错误的函数都会被停止并向上传播,扰乱了代码的正常流程。try/catch虽然这看起来不是什么大问题,特别是对于较小的应用程序来说,但随着你的代码不断地在try/catch ,可读性和整体性能将下降。

让我们试着用try/catch 方法来解决我们天气例子中的错误。

const getWeather = async (
  city: string
): Promise<{ temp: number; humidity: number }> => {
  try {
    const temp = await externalTemperatureAPI(city);
    try {
      const humidity = await externalHumidityAPI(city);
    } catch (error) {
      console.log(`Error fetching humidity for ${city}`);
      return new Error(`Error fetching humidity for ${city}`);
    }
    return { temp, humidity };
  } catch (error) {
    console.log(`Error fetching temperature for ${city}`);
    return new Error(`Error fetching temperature for ${city}`);
  }
};
// ...
try {
  const weather = await getWeather("Berlin");
} catch (error) {
  console.log("getWeather() failed");
}

在上面的代码块中,当我们试图访问externalTemperatureAPIexternalHumidityAPI 时,我们在console.log 中遇到了两个错误,然后这些错误被停止并向上传播了几次。

结果类

当你使用上面讨论的两种错误处理方法中的任何一种时,一个简单的错误就会在原始错误的基础上增加不必要的复杂性。在返回null和抛出try/catch ,所产生的问题在其他前端语言中很常见,比如Kotlin、Rust和C#。这三种语言使用结果类作为一个相当普遍的解决方案。

无论执行是成功还是失败,Result类将封装给定函数的结果,允许函数调用者将错误作为正常执行流程的一部分来处理,而不是作为一个异常。

当与TypeScript配对时,结果类提供了类型安全和关于一个函数可能导致的错误的详细信息。当我们修改一个函数的错误结果时,结果类为我们提供了代码库中受影响地方的编译时错误。

让我们回头看看我们的天气例子。我们将使用Rust的结果和选项对象的TypeScript实现,ts-results。

还有其他TypeScript的包,具有非常类似的API,比如NeverThrow,所以你应该随意玩玩。

import { Ok, Err, Result } from "ts-results";

type Errors = "CANT_FETCH_TEMPERATURE" | "CANT_FETCH_HUMIDITY";

const getWeather = async (
  city: string
): Promise<Result<{ temp: number; humidity: number }, Errors>> => {
  const temp = await externalTemperatureAPI(city);
  if (!temp) return Err("CANT_FETCH_TEMPERATURE");

  const humidity = await externalHumidityAPI(city);
  if (!humidity) return Err("CANT_FETCH_HUMIDITY");

  return Ok({ temp, humidity });
};

// ...

const weatherResult = await getWeather("Berlin"); // weatherResult is fully typed
if (weatherResult.err) console.log(`getWeather() failed: ${weatherResult.val}`);
if (weatherResult.ok) console.log(`Weather is: ${JSON.stringify(weather.val)}`);

从函数中添加类型安全的结果,并在我们的代码中优先处理错误,这与我们以前的例子相比是一个进步,然而,我们仍然有工作要做让我们探讨一下如何使我们对结果错误的类型检查变得详尽。

需要注意的是,偏爱Result类并不意味着你不会使用try/catch 结构!当你与外部包一起工作时,仍然需要try/catch 结构。

如果你认为结果类是一个值得遵循的方法,你可以尝试将这些接触点封装在模块中,并在内部使用结果类。

添加详尽的类型检查

当交出可以返回多个错误结果的函数时,提供覆盖所有错误情况的类型检查会很有帮助。这样做可以确保函数的调用者可以对错误的类型做出动态反应,并且可以确定没有错误情况被忽略。

我们可以通过一个详尽的开关来实现这一点。

// Exhaustive switch helper
class UnreachableCaseError extends Error {
  constructor(val: never) {
    super(`Unreachable case: ${val}`);
  }
}

// ...

const weatherResult = getWeather("Berlin");
if (weatherResult.err) {
  // handle errors
  const errValue = weatherResult.val;
  switch (errValue) {
    case "CANT_FETCH_TEMPERATURE":
      console.error("getWeather() failed with: CANT_FETCH_TEMPERATURE");
      break;
    case "CANT_FETCH_HUMIDITY":
      console.error("getWeather() failed with: CANT_FETCH_HUMIDITY");
      break;
    default:
      throw new UnreachableCaseError(errValue); // runtime type check for catching all errors
  }
}

当我们用穷举式开关运行我们的天气例子时,它将在两组情况下提供编译时错误。一种是没有处理所有的错误情况,另一种是原始函数中的错误发生变化。

总结

现在,你知道了在TypeScript中处理常见错误的改进方案了吧!你知道吗?知道了错误处理的重要性,我希望你能用这个方法来获得关于你的应用程序中任何错误的最具体信息。

在本教程中,我们介绍了一些普遍存在的方法的缺点,如返回null和throw/catch 方法。最后,我们学习了如何使用TypeScript结果类,用详尽的开关来捕捉错误。

The postImproving TypeScript error handling with exhaustive type checkingappeared first onLogRocket Blog.