TypeScript中如何判断函数多类型参数的实际传入类型?
这确实是TypeScript处理联合类型时很常见的困惑——既要准确收窄类型,又不想写一堆冗余代码。咱们一步步来拆解最合适的方案:
优先利用类型本身的结构差异做类型守卫
你定义的A/B/C/D其实本身就有明显的结构差异,完全可以用TypeScript内置的类型守卫语法(typeof、in操作符)来直接判断,不需要额外给类型加type/kind属性,也不用写自定义守卫函数。
比如你的例子中:
A的value是字符串类型,可以用typeof parameter.value === 'string'判断B的value是数字类型,用typeof parameter.value === 'number'判断C的value有title属性,用'title' in parameter.value判断D的value有subtitle属性,用'subtitle' in parameter.value判断
修改后的函数代码完全合法,而且TS会自动帮你收窄类型:
interface SomeCustomInterface { title: string } interface OtherCustomInterface { subtitle: string } interface A { value: string } interface B { value: number } interface C { value: SomeCustomInterface } interface D { value: OtherCustomInterface } function doSomething(parameter: A | B | C | D): string { if (typeof parameter.value === 'string') { // TS知道这里parameter是A类型 return parameter.value; } else if (typeof parameter.value === 'number') { // TS知道这里parameter是B类型 return parameter.value.toString(); } else if ('title' in parameter.value) { // TS知道这里parameter是C类型 return parameter.value.title; } else { // TS知道这里parameter是D类型(因为前面已经排除了其他三种) return parameter.value.subtitle; } }
这种方式几乎没有额外开销,完全依赖类型本身的结构,代码也简洁。
结构差异不明显时用可辨识联合类型(减少冗余版)
如果你的类型之间结构差异很小,比如两个接口都有value但都是对象,只是内部属性不同,这时候可辨识联合类型(给每个类型加一个唯一的标识属性)确实是更清晰的方案,但可以通过交叉类型来减少冗余代码:
// 定义一个基础的带类型标识的接口 type Typed<T extends string> = { type: T }; interface SomeCustomInterface { title: string } interface OtherCustomInterface { subtitle: string } // 用交叉类型复用基础标识,只定义差异部分 type A = Typed<'A'> & { value: string }; type B = Typed<'B'> & { value: number }; type C = Typed<'C'> & { value: SomeCustomInterface }; type D = Typed<'D'> & { value: OtherCustomInterface }; function doSomething(parameter: A | B | C | D): string { switch (parameter.type) { case 'A': return parameter.value; case 'B': return parameter.value.toString(); case 'C': return parameter.value.title; case 'D': return parameter.value.subtitle; default: // 这里可以加一个never类型的断言,确保覆盖所有情况 const _exhaustiveCheck: never = parameter; throw new Error(`Unexpected type: ${_exhaustiveCheck}`); } }
这种方式只需要写一次基础的Typed类型,后续的类型定义只需要关注差异,冗余度很低,而且switch的写法非常直观,TS也能完美收窄类型。
自定义类型守卫的优化思路(可选)
如果确实需要自定义类型守卫(比如判断逻辑复杂),也可以通过复用逻辑减少重复代码。比如针对你的例子,可以写一个通用的守卫函数:
function isTypeWithValueProp<T>(obj: unknown, prop: keyof T): obj is { value: T } { return typeof obj === 'object' && obj !== null && 'value' in obj && prop in (obj as { value: unknown }).value; } // 使用时 function doSomething(parameter: A | B | C | D): string { if (typeof parameter.value === 'string') { return parameter.value; } else if (typeof parameter.value === 'number') { return parameter.value.toString(); } else if (isTypeWithValueProp<SomeCustomInterface>(parameter, 'title')) { return parameter.value.title; } else { return parameter.value.subtitle; } }
不过这种方式只适合逻辑复杂的场景,大多数情况下前面两种方案已经足够。
总结一下:优先用类型本身的结构差异做判断,零冗余;如果结构差异不明显,用带交叉类型的可辨识联合,尽量减少重复代码;自定义守卫作为最后选项。
内容的提问来源于stack exchange,提问作者matteok
相关产品推荐
相关产品推荐

