TypeScript中为何无法收窄单值/数组联合类型?如何解决?
TypeScript联合类型收窄问题:为什么无法通过数组判断完成类型收窄?
我可以轻松用字符串常量收窄联合类型:
type Payload1 = { /* ... 任意类型 ... */ }; type Payload2 = { /* ... 任意类型 ... */ }; type T1 = { type: 'type1', payload: Payload1 } type T2 = { type: 'type2', payload: Payload2 } type T = T1 | T2; const fn = (value: T) => { if (value.type === 'type1') { value; // TypeScript 知道 value 是 T1 } if (value.type === 'type2') { value; // TypeScript 知道 value 是 T2 } };
这里只有两种情况:
value.type为常量"type1"value.type为常量"type2"
但如果扩展T,允许payload是单值或数组,就会出现四种可能性:
value.type为"type1"且value.payload不是数组value.type为"type1"且value.payload是数组value.type为"type2"且value.payload不是数组value.type为"type2"且value.payload是数组
示例代码如下:
type Payload1 = {}; type Payload2 = {}; type T1Single = { type: 'type1', payload: Payload1 } type T1Batch = { type: 'type1', payload: Payload1[] }; type T2Single = { type: 'type2', payload: Payload2 } type T2Batch = { type: 'type2', payload: Payload2[] }; // 现在 T 包含4种类型而不是2种: type T = T1Single | T1Batch | T2Single | T2Batch; const fn = (value: T) => { if (value.type === 'type1' && !Array.isArray(value.payload)) { value; // TypeScript 说 value 是 T1Single | T1Batch?! // 明明 payload 不是数组,为什么 T1Batch 还在联合类型里? } if (value.type === 'type1' && Array.isArray(value.payload)) { value; // TypeScript 说 value 是 T1Single | T1Batch?! // 明明 payload 是数组,为什么 T1Single 还在联合类型里? } if (value.type === 'type2' && !Array.isArray(value.payload)) { value; // TypeScript 说 value 是 T2Single | T2Batch?! // 明明 payload 不是数组,为什么 T2Batch 还在联合类型里? } if (value.type === 'type2' && Array.isArray(value.payload)) { value; // TypeScript 说 value 是 T2Single | T2Batch?! // 明明 payload 是数组,为什么 T2Single 还在联合类型里? } };
为什么TypeScript只能部分收窄类型?如何实现这四种情况的完整类型收窄?
编辑补充:就算if里只使用Array.isArray,TypeScript也没法完成收窄:
type Payload = {}; type Single = { payload: Payload } type Batch = { payload: Payload[] }; const fn = (value: Single | Batch) => { if (!Array.isArray(value.payload)) { value; // TypeScript 说 value 是 Single | Batch?! } if (Array.isArray(value.payload)) { value; // TypeScript 说 value 是 Single | Batch?! } };
原因分析
TypeScript的类型收窄依赖类型守卫的精确性,这里的核心问题是:Payload(或Payload1/Payload2)被定义为{},在TypeScript中,{}类型表示“任何非null/undefined的值”——数组本质上也是对象,因此Payload[]属于Payload的子类型。
比如const arr: {} = []是完全合法的,这就导致Array.isArray(value.payload)无法让TypeScript区分payload是单值还是数组:因为数组类型也符合单值Payload的约束,类型守卫无法排除其中一方。
解决方案
要实现完整类型收窄,需要让单值Payload和数组Payload[]成为互斥类型,让TypeScript能明确区分两者。
方案1:给单值Payload添加专属属性
给单值类型添加数组没有的标识属性,让类型产生明确区分:
type Payload1 = { isSingle: true; /* 其他业务属性 */ }; type Payload2 = { isSingle: true; /* 其他业务属性 */ }; type T1Single = { type: 'type1', payload: Payload1 } type T1Batch = { type: 'type1', payload: Payload1[] }; type T2Single = { type: 'type2', payload: Payload2 } type T2Batch = { type: 'type2', payload: Payload2[] }; type T = T1Single | T1Batch | T2Single | T2Batch; const fn = (value: T) => { if (value.type === 'type1' && value.payload.isSingle) { value; // TypeScript 正确识别为 T1Single } if (value.type === 'type1' && Array.isArray(value.payload)) { value; // TypeScript 正确识别为 T1Batch } };
方案2:用类型工具排除数组可能性
通过never类型让单值Payload无法兼容数组:
type NotArray<T> = T extends Array<any> ? never : T; type Payload1 = NotArray<{ /* 其他业务属性 */ }>; type Payload2 = NotArray<{ /* 其他业务属性 */ }>; type T1Single = { type: 'type1', payload: Payload1 } type T1Batch = { type: 'type1', payload: Payload1[] }; type T2Single = { type: 'type2', payload: Payload2 } type T2Batch = { type: 'type2', payload: Payload2[] }; type T = T1Single | T1Batch | T2Single | T2Batch; const fn = (value: T) => { if (value.type === 'type1' && !Array.isArray(value.payload)) { value; // 正确识别为 T1Single } };
方案3:自定义类型守卫函数
手动编写类型谓词,明确告诉TypeScript如何判断目标类型:
type Payload1 = {}; type Payload2 = {}; type T1Single = { type: 'type1', payload: Payload1 } type T1Batch = { type: 'type1', payload: Payload1[] }; type T2Single = { type: 'type2', payload: Payload2 } type T2Batch = { type: 'type2', payload: Payload2[] }; type T = T1Single | T1Batch | T2Single | T2Batch; function isT1Single(value: T): value is T1Single { return value.type === 'type1' && !Array.isArray(value.payload); } function isT1Batch(value: T): value is T1Batch { return value.type === 'type1' && Array.isArray(value.payload); } const fn = (value: T) => { if (isT1Single(value)) { value; // 正确识别为 T1Single } if (isT1Batch(value)) { value; // 正确识别为 T1Batch } };
内容的提问来源于stack exchange,提问作者Gershom Maes
相关产品推荐
相关产品推荐

