带可选判别式的TypeScript区分联合类型在React组件中的类型收窄问题
问题场景
先看这段简化后的代码:
类型定义
type Client = { kind?: 'client', fn: (updatedIds: string[]) => void }; type Server = { kind: 'server', fn: (selectionListId: string) => void }; type Thing = Client | Server;
组件代码
function MyComponent(props: Thing) { const { kind = 'client', fn } = props; if (kind === 'client') { props.fn(['hey']); // 报错 // fn(['hey']) 同样报错 } else { props.fn('hi') // 报错 // fn('hey') 同样报错 } }
核心疑问:明明给kind设了默认值,TypeScript却无法正确收窄Thing类型,反而把两个分支里fn的类型判定为(arg0: string[] & string) => void——也就是取参数类型的交集。预期要么是分支非穷尽报错,要么只有else分支报错,结果两个分支都报这个奇怪的交集类型错,想搞懂内部原理,还要更优雅的解决办法。
原理分析
1. 类型收窄失效的根源
区分联合类型(Discriminated Union)的核心要求是:判别式必须是明确的字面量类型,且不可选。这里Client的kind是可选的,导致TypeScript无法把它当成可靠的判别依据——它无法确定kind的undefined是属于Client的可选属性,还是其他情况。
另外,解构时的kind = 'client'是运行时赋值,TypeScript的类型检查器没办法把这个默认值和原联合类型的定义关联起来。它只知道kind变量的类型是'client' | 'server',但无法反向推导props的类型会跟着kind变量的取值收窄。
2. 函数参数交集类型的由来
当TypeScript无法确定联合类型的具体分支时,会对联合类型中的函数类型做逆变处理:为了保证函数调用在任何分支下都安全,它会取所有函数参数类型的交集。
这里Client的fn参数是string[],Server的是string,它们的交集string[] & string是never(没有值能同时是数组和字符串),所以TypeScript会提示你无法传入任何有效的参数——这本质是TypeScript在告诉你:当前无法确定fn的具体类型,任何调用都可能不兼容。
优雅解决方案
方案1:修正区分联合类型定义(最推荐)
把Client的kind改成必填的字面量类型,让TypeScript能正确识别判别式:
type Client = { kind: 'client', // 去掉可选标记 fn: (updatedIds: string[]) => void }; type Server = { kind: 'server', fn: (selectionListId: string) => void }; type Thing = Client | Server; // 组件调用时如果是Client类型,依然可以通过默认值省略kind function MyComponent({ kind = 'client', fn }: Thing) { if (kind === 'client') { fn(['hey']); // 类型完全匹配,无报错 } else { fn('hi'); // 类型完全匹配,无报错 } }
这样TypeScript能通过kind的字面量完全收窄类型,函数调用不再有问题。
方案2:用类型谓词手动收窄
如果必须保留kind的可选性,可以用自定义类型谓词明确判断类型:
function isClient(props: Thing): props is Client { return props.kind === undefined || props.kind === 'client'; } function MyComponent(props: Thing) { const { fn } = props; if (isClient(props)) { fn(['hey']); // 正确识别为Client的fn } else { fn('hi'); // 正确识别为Server的fn } }
这种方式让TypeScript明确知道每个分支对应的具体类型,解决收窄失效问题。
方案3:先收窄类型再解构
跳过解构时的默认值,先对props进行类型收窄,再处理:
function MyComponent(props: Thing) { if (props.kind === 'server') { props.fn('hi'); // 明确是Server类型 } else { // 这里props被自动收窄为Client类型(包括kind为undefined的情况) props.fn(['hey']); } }
内容的提问来源于stack exchange,提问作者Ryno

