TypeScript中ConditionalType为何解析结果不符合预期?
never的原因 示例代码
type PrimitiveDataType = string | number | bigint | boolean | symbol | undefined | null; type ConditionalType<T> = T extends PrimitiveDataType ? (v: T) => void : never; abstract class AbstractClass<T> { abstract value: T; protected conditionalFunctions: Map<ConditionalType<T>, number | undefined> = new Map(); } class SomeClass<T extends PrimitiveDataType> extends AbstractClass<T> { value: T; constructor(value: T) { super(); this.value = value; } someMethod() { for (const someFn of this.conditionalFunctions.keys()) { someFn(this.value); } } }
错误信息
TS2345: Argument of type 'PrimitiveDataType' is not assignable to parameter of type 'never'. Type 'undefined' is not assignable to type 'never'.
问题描述
我定义了PrimitiveDataType作为所有原始类型的联合,ConditionalType<T>在T属于原始类型时返回回调类型。接着创建泛型抽象类AbstractClass<T>,其中conditionalFunctions的类型依赖于泛型T。最后SomeClass<T>继承它并给T加上extends PrimitiveDataType的约束,但TypeScript却把conditionalFunctions解析为Map<(v: never) => void, number | undefined>,而不是预期的Map<(v: T) => void, number | undefined>。
当PrimitiveDataType只包含单一类型时一切正常,多类型联合就会报错。
编辑1:多分支条件类型的问题
如果条件类型有多个分支,用[T]避免分布式条件类型后,又出现新错误:
Expected 2 arguments, but got 1.
示例代码
type PrimitiveDataType = string | number | bigint | boolean | symbol | undefined | null; type ConditionalType<T> = [T] extends [PrimitiveDataType] ? (v: T) => void : T extends Array<unknown> ? (v: T, t: number) => void : never; abstract class AbstractClass<T> { abstract value: T; protected conditionalFunctions: Map<ConditionalType<T>, number | undefined> = new Map(); } class SomeClass<T extends PrimitiveDataType> extends AbstractClass<T> { value: T; constructor(value: T) { super(); this.value = value; } someMethod() { for (const someFn of this.conditionalFunctions.keys()) { someFn(this.value); } } }
但单独测试条件类型时是正常的:
type PrimitiveDataType = string | number | bigint | boolean | symbol | undefined | null; type ConditionalType<T> = [T] extends [PrimitiveDataType] ? (v: T) => void : T extends Array<unknown> ? (v: T, t: number) => void : never; const x: PrimitiveDataType = 12; const y: ConditionalType<typeof x> = (param: number) => undefined;
这里ConditionalType<typeof x>被正确解析为(v: number) => void。
原因分析
原始问题:分布式条件类型的影响
T extends PrimitiveDataType这种写法会触发分布式条件类型,当T是联合类型时,TypeScript会把联合类型拆成单个成员分别计算条件类型,再把结果合并成新的联合。
在AbstractClass<T>中,T没有约束,所以当SomeClass传入T extends PrimitiveDataType(即T是原始类型的联合)时,ConditionalType<T>会被展开为多个单参数函数的联合类型。但Map的键类型不能是函数联合类型——因为函数是引用类型,联合函数作为Map键无法被TypeScript正确识别为可比较的键,最终会被降级解析为never,这就是报错的根源。
编辑1的问题:条件类型的延迟解析
当用[T] extends [PrimitiveDataType]关闭分布式条件类型后,ConditionalType<T>变成了一个延迟解析的条件类型。由于AbstractClass中的T是无约束泛型,TypeScript无法提前确定它到底是单参数还是双参数函数。即使SomeClass给T加了约束,AbstractClass中的类型定义不会被重新推导,导致someFn的类型被识别为两种函数类型的联合,调用时TypeScript会要求传入所有分支需要的参数,因此报错“Expected 2 arguments”。
解决方案
针对原始问题:提前约束泛型或调整写法
- 给抽象类的泛型加约束
直接在AbstractClass中给T加上extends PrimitiveDataType的约束,让TypeScript明确ConditionalType<T>只会是单参数函数:
type PrimitiveDataType = string | number | bigint | boolean | symbol | undefined | null; type ConditionalType<T extends PrimitiveDataType> = (v: T) => void; abstract class AbstractClass<T extends PrimitiveDataType> { abstract value: T; protected conditionalFunctions: Map<ConditionalType<T>, number | undefined> = new Map(); } class SomeClass<T extends PrimitiveDataType> extends AbstractClass<T> { value: T; constructor(value: T) { super(); this.value = value; } someMethod() { for (const someFn of this.conditionalFunctions.keys()) { someFn(this.value); } } }
- 关闭分布式条件类型并添加类型断言
如果AbstractClass需要支持非原始类型,用[T]关闭分布式,同时在调用时用类型断言缩小范围:
type PrimitiveDataType = string | number | bigint | boolean | symbol | undefined | null; type ConditionalType<T> = [T] extends [PrimitiveDataType] ? (v: T) => void : T extends Array<unknown> ? (v: T, t: number) => void : never; abstract class AbstractClass<T> { abstract value: T; protected conditionalFunctions: Map<ConditionalType<T>, number | undefined> = new Map(); } class SomeClass<T extends PrimitiveDataType> extends AbstractClass<T> { value: T; constructor(value: T) { super(); this.value = value; } someMethod() { for (const someFn of this.conditionalFunctions.keys()) { (someFn as (v: T) => void)(this.value); } } }
多分支场景的优雅写法:类型守卫
用类型守卫区分不同分支的函数类型,避免联合类型的歧义:
type PrimitiveDataType = string | number | bigint | boolean | symbol | undefined | null; type ConditionalType<T> = T extends PrimitiveDataType ? (v: T) => void : T extends Array<unknown> ? (v: T, t: number) => void : never; abstract class AbstractClass<T> { abstract value: T; protected conditionalFunctions: Map<ConditionalType<T>, number | undefined> = new Map(); protected isPrimitiveFn(fn: ConditionalType<T>): fn is (v: T) => void { return typeof fn === 'function' && fn.length === 1; } protected isArrayFn(fn: ConditionalType<T>): fn is (v: T, t: number) => void { return typeof fn === 'function' && fn.length === 2; } } class SomeClass<T extends PrimitiveDataType> extends AbstractClass<T> { value: T; constructor(value: T) { super(); this.value = value; } someMethod() { for (const someFn of this.conditionalFunctions.keys()) { if (this.isPrimitiveFn(someFn)) { someFn(this.value); } } } }
内容的提问来源于stack exchange,提问作者Krzysztof Kaczyński

