TypeScript 错误处理:用 Result 类型替代 try-catch
摘要
TypeScript 中的 try-catch 模式可能会隐藏类型安全陷阱,因为 catch 块中的错误类型是 unknown。自定义错误类可以解决这个问题,但也存在维护问题。Result 类型可以提供更优雅的解决方案,通过可辨识联合类型和 const 类型参数实现类型安全的错误处理。这种方法可以让 TypeScript 在编译时就告诉你有哪些错误需要处理,漏了就报错。Result 类型的优点包括类型安全、编译时检查和低维护成本。
在 TypeScript 的开发中,抛出错误然后由调用方 try-catch 捕获是非常常见的模式。然而,这个我们习以为常的模式,其实隐藏着一个巨大的类型安全陷阱。
try-catch 的类型困境
看看下面这段再普通不过的代码:
在 catch 块中,e 的类型是 unknown。你完全无法知道它到底是什么错误,更别提根据不同的错误类型做不同的处理了。
这在其他语言中并不是问题。比如 Java 会在函数签名中通过 throws 关键字明确声明可能抛出的异常类型;Rust 和 Go 则直接让你返回错误,强制你在调用处处理它。而 TypeScript 呢?什么都不告诉你。
自定义错误类:一个看似优雅的方案
你可能会想:"我定义几个自定义错误类不就好了?"
然后在函数中抛出这些自定义错误:
在调用处用 instanceof 来区分处理:
看起来还不错对吧?但这么做有个巨大的隐患。
隐患在哪?
因为抛出错误的地方和处理错误的地方是完全分离的。当你(或者其他开发者)日后在 somethingThatWillThrowError 函数里新增了一种自定义错误,或者删除了某个已有的错误类型,你还得记着去所有调用它的地方同步修改 catch 中的处理逻辑。
这非常不可靠 —— 就算忘记处理了,编译也完全不会报错。对于追求类型安全的 TypeScript 项目来说,这是不可接受的。
更优雅的方案:Result 类型
那有没有一种方式,能让 TypeScript 在编译时就告诉你有哪些错误需要处理,漏了就报错?
有的兄弟,有的!那就是 Result 类型。
定义 Result 类型
先来看看 Result 类型长什么样:
这是一个很简洁的可辨识联合类型(Discriminated Union)。它表达了一个非常清晰的语义:一个操作的结果,要么是成功的数据,要么是带有 reason 的错误,二者不会同时存在。
接下来定义两个工具函数,让创建 Result 对象更加方便:
这里有个关键技巧:err 函数的泛型参数 TReason 前面加了一个 const 关键字。这是 TypeScript 5.0 引入的 const 类型参数,它告诉 TypeScript 将 TReason 推断为字面量类型(如 "MY_ERROR"),而不是宽泛的 string 类型。这是实现完整类型安全的关键一步。
改造函数
现在用 ok 和 err 来改造之前的函数 —— 不再 throw,而是 return:
注意,函数不再抛出错误,而是将错误作为返回值的一部分。TypeScript 会自动推断出这个函数的返回类型是:
所有可能的错误类型,都被编码在了返回类型中。
类型安全的错误处理
最后,在调用方可以这样优雅地处理错误:
为什么这能解决问题?
关键就在 default 分支中的 reason satisfies never。
satisfies 是 TypeScript 4.9 引入的关键字,它可以在不改变变量类型的前提下验证表达式是否满足某个类型。这里我们用来确保 reason 的类型已经被收窄为 never —— 也就是说,所有可能的错误都在上面的 case 中被处理了。
这带来了双向的编译时保护:
新增了错误但忘记处理?
reason的类型不会收窄为never,satisfies never会编译报错 ❌处理了一个已经被删除的错误? 对应的
case分支会报错,因为该字面量类型已不存在 ❌
实实在在的类型安全,不依赖人的记忆力,完全由编译器帮你兜底。
总结
方案 | 类型安全 | 编译时检查 | 维护成本 |
|---|---|---|---|
原生 try-catch | ❌ | ❌ 无 | 高 |
自定义错误 + instanceof | ⚠️ 部分 | ❌ 无 | 高 |
Result 类型 | ✅ 完全 | ✅ 穷尽检查 | 低 |
Result 类型并不是什么新鲜概念,它在 Rust(Result<T, E>)、Haskell(Either)等语言中早已是标准做法。在 TypeScript 中,借助可辨识联合类型、const 类型参数和 satisfies 关键字,我们同样可以实现优雅且类型安全的错误处理。
下次写 TypeScript 的时候,不妨试试用 Result 类型替代 try-catch,你的代码会更健壮,你的队友也会感谢你的。
