如何通过映射而非switch实现TypeScript可辨识联合类型收窄
TypeScript可辨识联合类型:无需switch的类型收窄方案
需求与常规实现
我需要基于数据的type属性渲染React组件,希望在处理TypeScript可辨识联合类型时,避免使用switch/if-else语句。常规写法中,switch可以让TypeScript自动完成类型收窄:
type AView = {type: 'A', name: string} type BView = {type: 'B', count: number} type View = AView | BView const prints = { 'A': (view: AView) => `this is A: ${view.name}`, 'B': (view: BView) => `this is B. Count: ${view.count}`, } as const const outputWorks = (view: View): string => { switch (view.type) { case 'A': return prints[view.type](view) case 'B': return prints[view.type](view) } } outputWorks({type: 'A', name: 'John'})
问题:直接使用映射对象报错
我尝试省去switch,直接通过映射对象调用对应函数,但TypeScript会报错:The intersection 'AView & BView' was reduced to 'never' because property 'type' has conflicting types in some constituents.,代码如下:
type AView = {type: 'A', name: string} type BView = {type: 'B', count: number} type View = AView | BView const prints = { 'A': (view: AView) => `this is A: ${view.name}`, 'B': (view: BView) => `this is B. Count: ${view.count}`, } as const const outputFail = (view: View): string => prints[view.type](view) outputFail({type: 'A', name: 'John'})
原因是TypeScript无法自动关联view.type与映射函数的参数类型,它会错误地认为函数需要接受所有联合类型的交集,而AView和BView的type属性冲突,交集为never。
解决方案:通过类型映射与泛型实现类型安全
我们可以通过类型映射明确每个type对应的View类型,再配合泛型函数让TypeScript正确关联类型:
type AView = { type: 'A', name: string } type BView = { type: 'B', count: number } type View = AView | BView // 定义映射类型,确保每个type对应正确的View子类型 type ViewHandlerMap = { [T in View['type']]: (view: Extract<View, { type: T }>) => string } // 用映射类型约束prints对象,无需手动指定参数类型 const prints: ViewHandlerMap = { 'A': (view) => `this is A: ${view.name}`, 'B': (view) => `this is B. Count: ${view.count}`, } // 泛型函数让TypeScript自动关联view的type与对应的处理函数 const outputSuccess = <T extends View['type']>(view: Extract<View, { type: T }>): string => { return prints[view.type](view) } // 正常工作,类型完全安全 outputSuccess({ type: 'A', name: 'John' }) outputSuccess({ type: 'B', count: 123 })
另一种简化写法:类型断言(不推荐,但更简洁)
如果追求代码简洁,也可以在调用时用类型断言告诉TypeScript当前view的类型与函数参数匹配:
const outputSuccess = (view: View): string => { return prints[view.type](view as never) }
不过这种方法跳过了部分类型检查,不如类型映射的方式严谨,适合简单场景。
内容的提问来源于stack exchange,提问作者Nikolai Lopin
相关产品推荐
相关产品推荐

