如何在TypeScript中实现穷尽类型守卫并兼顾公共接口?
同时实现Result类型的类型守卫与公共接口约束
现有方案的矛盾
方案1:基于继承的Result接口
这种定义通过让Ok和Err继承Result接口,强制约束两者必须实现isOk()和isErr()方法,结构规范清晰:
interface Result<T, E> { isOk(): this is Ok<T>; isErr(): this is Err<E>; } interface Ok<T> extends Result<T, never> { value: T; isOk(): true; isErr(): false; } interface Err<E> extends Result<never, E> { error: E; isOk(): false; isErr(): true; }
但缺点很明显:当用isOk()做类型检查后,未进入分支的代码里,result的类型依然是Result<T,E>,无法自动缩小为Err<E>,类型守卫没起到完全的作用。
方案2:联合类型定义Result
把Result直接定义为Ok和Err的联合类型,解决了类型缩小的问题:
interface Ok<T> { value: T; isOk(): true; } interface Err<E> { error: E; isOk(): false; } type Result<T, E> = Ok<T> | Err<E>;
现在执行if (result.isOk())检查后,分支外的result会自动被推断为Err<E>。但代价是Ok和Err没有公共的接口约束,无法强制要求两者必须实现统一的方法,也不能通过继承关系判断它们属于Result体系。
兼顾两者的解决方案
可以通过让Result同时作为接口和联合类型的方式,把两种方案的优势结合起来:
// 定义公共接口,约束Ok/Err必须实现的方法 interface Result<T, E> { isOk(): this is Ok<T>; isErr(): this is Err<E>; } // Ok继承Result接口,保证结构规范 interface Ok<T> extends Result<T, never> { value: T; isOk(): true; isErr(): false; } // Err同样继承Result接口 interface Err<E> extends Result<never, E> { error: E; isOk(): false; isErr(): true; } // 同时把Result定义为Ok和Err的联合类型,实现类型缩小 type Result<T, E> = Ok<T> | Err<E>;
为什么这个方案可行?
TypeScript允许同一个标识符既作为接口存在,又作为类型别名存在(只要两者的定义不冲突)。这里:
- 接口
Result负责约束Ok和Err必须实现isOk()、isErr()方法,保证了结构一致性; - 类型别名
Result<T,E>作为Ok<T>|Err<E>的联合类型,让TypeScript能够在类型守卫后正确缩小类型范围。
现在再执行类型检查:
function handleResult<T, E>(result: Result<T, E>) { if (result.isOk()) { // 这里result被推断为Ok<T>,可以直接访问value console.log(result.value); } else { // 这里result自动被推断为Err<E>,可以访问error console.log(result.error); } }
不管是if分支内还是分支外,类型都能正确缩小,同时Ok和Err必须遵守Result接口的约束,完美兼顾了两种方案的优势。
内容的提问来源于stack exchange,提问作者lishaak
相关产品推荐
相关产品推荐

