banner
约 1,500 字
5 分钟

TypeScript 错误处理:用 Result 类型替代 try-catch

2026年2月10日
2026年2月11日

摘要

TypeScript 中的 try-catch 模式可能会隐藏类型安全陷阱,因为 catch 块中的错误类型是 unknown。自定义错误类可以解决这个问题,但也存在维护问题。Result 类型可以提供更优雅的解决方案,通过可辨识联合类型和 const 类型参数实现类型安全的错误处理。这种方法可以让 TypeScript 在编译时就告诉你有哪些错误需要处理,漏了就报错。Result 类型的优点包括类型安全、编译时检查和低维护成本。

在 TypeScript 的开发中,抛出错误然后由调用方 try-catch 捕获是非常常见的模式。然而,这个我们习以为常的模式,其实隐藏着一个巨大的类型安全陷阱。

try-catch 的类型困境

看看下面这段再普通不过的代码:

TypeScript
try {
  somethingThatWillThrowError();
} catch (e) {
  // e 的类型是 unknown —— 完全不知道这是什么错误
  console.error(e);
}

catch 块中,e 的类型是 unknown。你完全无法知道它到底是什么错误,更别提根据不同的错误类型做不同的处理了。

这在其他语言中并不是问题。比如 Java 会在函数签名中通过 throws 关键字明确声明可能抛出的异常类型;Rust 和 Go 则直接让你返回错误,强制你在调用处处理它。而 TypeScript 呢?什么都不告诉你。

自定义错误类:一个看似优雅的方案

你可能会想:"我定义几个自定义错误类不就好了?"

TypeScript
class MyError extends Error {
  constructor(message: string = "My Error") {
    super(message);
    this.name = "MyError";
  }
}

class YourError extends Error {
  constructor(message: string = "Your Error") {
    super(message);
    this.name = "YourError";
  }
}

然后在函数中抛出这些自定义错误:

TypeScript
function somethingThatWillThrowError() {
  // ...
  if (someCondition) {
    throw new MyError();
  }
  // ...
  if (anotherCondition) {
    throw new YourError();
  }
}

在调用处用 instanceof 来区分处理:

TypeScript
try {
  somethingThatWillThrowError();
} catch (e) {
  if (e instanceof MyError) {
    // 处理 MyError...
  } else if (e instanceof YourError) {
    // 处理 YourError...
  } else {
    // 未知错误
  }
}

看起来还不错对吧?但这么做有个巨大的隐患

隐患在哪?

因为抛出错误的地方处理错误的地方是完全分离的。当你(或者其他开发者)日后在 somethingThatWillThrowError 函数里新增了一种自定义错误,或者删除了某个已有的错误类型,你还得记着去所有调用它的地方同步修改 catch 中的处理逻辑。

这非常不可靠 —— 就算忘记处理了,编译也完全不会报错。对于追求类型安全的 TypeScript 项目来说,这是不可接受的。

更优雅的方案:Result 类型

那有没有一种方式,能让 TypeScript 在编译时就告诉你有哪些错误需要处理,漏了就报错?

有的兄弟,有的!那就是 Result 类型

定义 Result 类型

先来看看 Result 类型长什么样:

TypeScript
export type Result<TData, TError extends { reason: string }> =
  | {
      data: TData;
      error: null;
    }
  | {
      error: TError;
      data: null;
    };

这是一个很简洁的可辨识联合类型(Discriminated Union)。它表达了一个非常清晰的语义:一个操作的结果,要么是成功的数据,要么是带有 reason 的错误,二者不会同时存在。

接下来定义两个工具函数,让创建 Result 对象更加方便:

TypeScript
export function ok<TData>(data: TData): Result<TData, never> {
  return { data, error: null };
}

export function err<
  const TReason extends string,
  TError extends { reason: TReason },
>(error: TError): Result<never, TError> {
  return { error, data: null };
}

这里有个关键技巧:err 函数的泛型参数 TReason 前面加了一个 const 关键字。这是 TypeScript 5.0 引入的 const 类型参数,它告诉 TypeScript 将 TReason 推断为字面量类型(如 "MY_ERROR"),而不是宽泛的 string 类型。这是实现完整类型安全的关键一步。

改造函数

现在用 okerr 来改造之前的函数 —— 不再 throw,而是 return

TypeScript
function somethingThatWillThrowError() {
  // ...
  if (someCondition) {
    return err({ reason: "MY_ERROR" });
  }
  // ...
  if (anotherCondition) {
    return err({ reason: "YOUR_ERROR" });
  }

  return ok("okokok");
}

注意,函数不再抛出错误,而是将错误作为返回值的一部分。TypeScript 会自动推断出这个函数的返回类型是:

TypeScript
Result<string, { reason: "MY_ERROR" }> |
  Result<string, { reason: "YOUR_ERROR" }>;

所有可能的错误类型,都被编码在了返回类型中。

类型安全的错误处理

最后,在调用方可以这样优雅地处理错误:

TypeScript
const result = somethingThatWillThrowError();

if (result.error) {
  const reason = result.error.reason;
  switch (reason) {
    case "MY_ERROR":
      // 处理 MY_ERROR
      break;
    case "YOUR_ERROR":
      // 处理 YOUR_ERROR
      break;
    default: {
      // 穷尽检查:如果所有错误都处理了,reason 的类型会收窄为 never
      console.error(`未处理的错误:${reason satisfies never}`);
    }
  }
  return;
}

// 走到这里,TypeScript 知道 result.error 是 null,result.data 一定有值
console.log(result.data); // 类型是 string

为什么这能解决问题?

关键就在 default 分支中的 reason satisfies never

satisfies 是 TypeScript 4.9 引入的关键字,它可以在不改变变量类型的前提下验证表达式是否满足某个类型。这里我们用来确保 reason 的类型已经被收窄为 never —— 也就是说,所有可能的错误都在上面的 case 中被处理了。

这带来了双向的编译时保护

  • 新增了错误但忘记处理? reason 的类型不会收窄为 neversatisfies never 会编译报错 ❌

  • 处理了一个已经被删除的错误? 对应的 case 分支会报错,因为该字面量类型已不存在 ❌

实实在在的类型安全,不依赖人的记忆力,完全由编译器帮你兜底。

总结

方案

类型安全

编译时检查

维护成本

原生 try-catch

unknown 类型

❌ 无

自定义错误 + instanceof

⚠️ 部分

❌ 无

Result 类型

✅ 完全

✅ 穷尽检查

Result 类型并不是什么新鲜概念,它在 Rust(Result<T, E>)、Haskell(Either)等语言中早已是标准做法。在 TypeScript 中,借助可辨识联合类型、const 类型参数和 satisfies 关键字,我们同样可以实现优雅且类型安全的错误处理。

下次写 TypeScript 的时候,不妨试试用 Result 类型替代 try-catch,你的代码会更健壮,你的队友也会感谢你的。

参考

END
© 2026 阿旷. All Rights Reserved. / RSS / Sitemap
Powered by Tanstack Start & Flare Stack Blog