TypeScript中如何理解逆变下的`F<T|U> = F<T> & F<U>`?
StackOverflow的《typescript intersection types of function》文章提到:
逆变会将事物转为对偶形式;若
F<T>对T逆变,则F<T | U>等价于F<T> & F<U>,F<T & U>等价于F<T> | F<U>。
我对这个等价性的证明方式存在困惑,以下是相关代码示例:
type Contra<T> = (arg: T) => any type T1 = Contra<string> & Contra<number> type T2 = Contra<string | number> type T3 = Contra<string> | Contra<number> declare let f1: T1; declare let f2: T2; f1(1); f1('1') f2(1); f2('1') f1 = f2; f2 = f1; // 无法将f1赋值给f2 declare let f3: T3; f3(1) // 正如预期是never,但如何直观理解?
一、如何理解F<T | U> ≡ F<T> & F<U>的等价性?
首先明确逆变的核心逻辑:函数的参数类型是逆变的——如果类型X是Y的子类型,那么接受Y的函数可以赋值给接受X的函数(能处理更宽泛类型的函数,必然能处理其子类型)。
回到示例:
Contra<string | number>是能接受string或number的函数,它的参数签名必须兼容两种类型的输入;Contra<string> & Contra<number>是同时满足“接受string”和“接受number”的函数(交叉类型表示函数必须具备两种类型的所有特性)。
从语义上,这两种类型完全等价:能处理string | number的函数,必然能单独处理string或number;反过来,能同时处理string和number的函数,也必然能处理它们的联合类型。
代码中f2 = f1报错是TypeScript的类型检查细节:交叉函数类型会被视为重载函数,重载函数赋值给单一签名的函数时,TypeScript需要确认每个重载都能兼容目标签名的参数。f1的两个重载分别只接受string或number,无法直接兼容string | number的联合参数(因为传入其中一种类型时,无法确定匹配哪个重载)。但从类型集合的角度,两者确实是等价的——只是TypeScript的类型系统在表达上有这个限制。
二、为什么f3(1)会提示never?
f3的类型是Contra<string> | Contra<number>,也就是这个函数要么只能接受string,要么只能接受number。
当调用f3(1)时,TypeScript无法确定f3的具体类型:
- 如果
f3是Contra<string>,传入number会报错; - 如果
f3是Contra<number>,传入number是合法的。
联合类型的调用需要满足所有分支都合法,因此只有当参数同时属于string和number时,调用才不会出错——但string和number的交集是never(不存在同时属于两种类型的值),所以f3的合法参数只能是never,也就是无法正常调用。
三、如何理解F<T & U> ≡ F<T> | F<U>的等价性?
用逆变逻辑推导:
T & U是同时属于T和U的窄类型(是T和U的子类型);Contra<T & U>是接受T & U的函数;Contra<T> | Contra<U>是要么接受T、要么接受U的函数。
根据逆变规则:
- 接受
T的函数可以赋值给接受T & U的函数(因为T & U是T的子类型,能处理T的函数必然能处理其子类型); - 同理,接受
U的函数也可以赋值给接受T & U的函数。
反过来,任何能接受T & U的函数,本质上都属于“能接受T”或“能接受U”的函数集合——因为T & U同时是T和U的子类型,能处理它的函数必然兼容其中至少一种父类型的输入。因此Contra<T & U>和Contra<T> | Contra<U>是等价的。
内容的提问来源于stack exchange,提问作者sixn

