关于上面这张图,有几点可以单独拿出来强调一下:
any
无处不在。它既是任何类型的子类型,也是任何类型的父类型,甚至可能是任意类型自己。所以,它可以赋值给任何类型;{}
充当 typeScript 类型的时候,它是有特殊含义的 - 它对应是(Object.prototype.__proto__)=null
在 js 原型链上的地位,它被视为所有的对象类型的基类。array
的字面量形式的子类型就是tuple
,function
的字面量形式的子类型就是函数表达式类型
。tuple
和函数表达式类型
都被囊括到字面量类型
中去。
现在我们用这个新的心智模型去理解一下 示例 2-1 报错的地方:
type Test1_1 = UselessType<number|string>
之所以报错,是因为在类型约束中,如果extends
前面的类型是联合类型,那么要想满足类型约束,则联合类型的每一个成员都必须满足类型约束才行。这就是所谓的「联合类型的分配律」。显然,string extends number
是不成立的,所以整个联合类型就不满足类型约束;- 对于对象类型的类型 - 即强调由属性和方法所组成的集合类型,我们需要先用面向对象的概念来确定两个类型中,谁是子类,谁是父类。这里的判断方法是 - 如果 A 类型相比 B 类型多出了一些属性/方法的话(这也同时意味着 B 类型拥有的属性或者方法,A 类型也必须要有),那么 A 类型就是父类,B 类型就是子类。然后,我们再转换到子类型和父类型的概念上来 - 父类就是「父类型」,子类就是「子类型」。
type Test2_1 = UselessType2<{a:1}>
之所以报错,是因为{a:1}
是{a:1, b:2}
的父类型,所以是不能赋值给{a:1, b:2}
;{[key:string]: any}
并不能成为{a:1, b:2}
的子类型,因为,父类型有的属性/方法,子类型必须显式地拥有。{[key:string]: any}
没有显式地拥有,所以,它不是{a:1, b:2}
的子类型,而是它的父类型。type Test3 = UselessType3<{name: '鲨叔'}>
和type Test3_2 = UselessType3<BaseClass>
报错的原因也是因为因为缺少了相应的属性/方法,所以,它们都不是SubClass
的子类型。
到这里,我们算是剖析完毕。下面总结一下。
- 当
extends
紧跟在泛型形参后面时,它是在表达「类型约束」的语义; - 在
AType extends BType
中,只有AType
是BType
的子类型,ts 通过类型约束的检验; - 面对两个 typeScript 类型,到底谁是谁的子类型,我们可以根据上面给出的 「ts 类型层级关系图」来判断。而对于一些充满迷惑的边缘用例,死记硬背即可。
extends 与条件类型
众所周知,ts 中的条件类型就是 js 世界里面的「三元表达式」。只不过,相比值世界里面的三元表达式最终被计算出一个「值」,ts 的三元表达式最终计算出的是「类型」。下面,我们先来复习一下它的语法:
AType extends BType ? CType : DType
在这里,extends
关键字出现在三元表达的第一个子句中。按照我们对 js 三元表达式的理解,我们对 typeScript 的三元表达式的理解应该是相似的:如果 AType extends BType
为逻辑真值,那么整个表达式就返回 CType
,否则的话就返回DType
。作为过来人,只能说,大部分情况是这样的,在几个边缘 case 里面,ts 的表现让你大跌眼镜,后面会介绍。
跟 js 的三元表达式支持嵌套一样,ts 的三元表达式也支持嵌套,即下面也是合法的语法:
AType extends BType ? (CType extends DType ? EType : FType) : (GType extends HType ? IType : JType)
到这里,我们已经看到了 typeScript 的类型编程世界的大门了。因为,三元表达式本质就是条件-分支语句,而后者就是逻辑编辑世界的最基本的要素了。而在我们进入 typeScript 的类型编程世界之前,我们首要搞清楚的是,AType extends BType
何时是逻辑上的真值。
幸运的是,我们可以复用「extends 与类型约束」上面所产出的心智模型。简而言之,如果 AType
是 BType
的子类型,那么代码执行就是进入第一个条件分支语句,否则就会进入第二个条件分支语句。
上面这句话再加上「ts 类型层级关系图」,我们几乎可以理解AType extends BType
99% 的语义。还剩下 1% 就是那些违背正常人直觉的特性表现。下面我们重点说说这 1% 的特性表现。
extends 与 {}
我们开门见山地问吧:“请说出下面代码的运行结果。”
type Test = 1 extends {} ? true : false // 请问 `Test` 类型的值是什么?
如果你认真地去领会上面给出的「ts 类型层级关系图」,我相信你已经知道答案了。如果你是基于「鸭子辩型」的直观理解去判断,那么我相信你的答案是true
。但是我的遗憾地告诉你,在 typeScript@4.9.4中,答案是false
。这明显是违背人类直觉的。于是乎,你会有这么一个疑问:“字面量类型 1
跟 {}
类型似乎牛马不相及,既不形似,也不神似,它怎么可能是是「字面量空对象」的子类型呢?”
好吧,就像我们在上一节提过的,{}
在 typeScript 中,不应该被理解为字面量空对象。它是一个特殊存在。它是一切有值类型的基类。ts 对它这么定位,似乎也合理。因为呼应了一个事实 - 在 js 中,一切都是对象 (字面量 1
在 js 引擎内部也是会被包成一个对象 - Number()的实例)。
现在,你不妨拿别的各种类型去测试一下它跟 {}
的关系,看看结果是不是跟我说的一样。最后,有一个注意点值的强调一下。假如我们忽略无处不在,似乎是百变星君的 any
,{}
的父类型只有一个 - unknown
。不信,我们可以试一试:
type Test = unknown extends {} ? true : false // `Test` 类型的值是 `false`
Test2
类型的值是 false
,从而证明了unknown
是{}
的父类型。
extends 与 any
也许你会觉得,extends
与 any
有什么好讲得嘛。你上面不是说了「any
」既是所有类型的子类型,又是所有类型的父类型。所以,以下示例代码得到的类型一定是true
:
type Test = any extends number ? true : false
额......在 typeScript@4.9.4 中, 结果似乎不是这样的 - 上面示例代码的运行结果是boolean
。这到底是怎么回事呢?这是因为,在 typeScript 的条件类型中,当any
出现在 extends
前面的时候,它是被视为一个联合里类型。这个联合类型有两个成员,一个是extends
后面的类型,一个非extends
后面的类型。还是用上面的示例举例子:
type Test = any extends number ? true : false
// 其实等同于
type Test = (number | non-number) extends number ? true : false
// 根据联合类型的分配率,展开得到
type Test = (number extends number ? true : false) | (non-number extends number ? true : false)
= true | false
= boolean
// 不相信我?我们再来试一个例子:
type Test2 = any extends number ? 1 : 2
// 其实等同于
type Test2 = (number | non-number) extends number ? 1 : 2
// 根据联合类型的分配率,展开得到
type Test = (number extends number ? 1 : 2) | (non-number extends number ? 1 : 2)
= 1 | 2
也许你会问,如果把 any
放在后面呢?比如:
type Test = number extends any ? true : false
这种情况我们可以依据 「任意类型都是any
的子类型」得到最终的结果是true
。
关于 extends 与 any 的运算结果,总结一下,总共有两种情况:
any extends SomeType(非 any 类型) ? AType : BType
的结果是联合类型AType | BType
SomeType(可以包含 any 类型) extends any ? AType : BType
的结果是AType
extends 与 never
在 typeScript 的三元表达式中,当 never
遇见 extends
,结果就变得很有意思了。可以换个角度说,是很奇怪。假设,我现在要你实现一个 typeScript utility 去判断某个类型(不考虑any
)是否是never
的时候,你可能会不假思索地在想:因为 never
是处在 typeScript 类型层级的最底层,也就是说,除了它自己,没有任何类型是它的子类型。所以答案肯定是这样:
type IsNever<T> = T extends never ? true : false
然后,你信心满满地给泛型形参传递个never
去测试,你发现结果是never
,而不是true
或者false
:
type Test = IsNever<never> // Test 的值为 `never`, 而不是我们期待的 `true`
再然后,你不甘心,你写下了下面的代码去进行再次测试:
type Test = never extends never ? true : false // Test 的值为 `true`, 符合我们的预期
你会发现,这次的结果却是符合我们的预期的。此时,你脑海里面肯定有千万匹草泥马奔腾而过。是的,ts 类型系统中,某些行为就是那么的匪夷所思。
对于这种违背直觉的特性表现,当前的解释是:当 never
充当实参去实例化泛型形参的时候,它被看作没有任何成员的联合类型。当 tsc 对没有成员的联合类型执行分配律时,tsc 认为这么做没有任何意义,所以就不执行这段代码,直接返回 never
。
那正确的实现方式是什么啊?是这个:
type IsNever<T> = [T] extends [never] ? true : false
原理是什么啊?答曰:「通过放入 tuple 中,消除了联合类型碰上 extends
时所产生的分配律」。
extends 与 联合类型
上面也提到了,在 typeScript 三元表达中,当 extends
前面的类型是联合类型的时候,ts 就会产生类似于「乘法分配律」行为表现。具体可以用下面的示例来表述:
type Test = (AType | BType) extends SomeType ? 'yes' : 'no'
= (AType extends SomeType ? 'yes' : 'no') | (BType extends SomeType ? 'yes' : 'no')
我们再来看看「乘法分配律」:(a+b)*c = a*c + b*c
。对比一下,我们就是知道,三元表达式中的 |
就是乘法分配律中的 +
, 三元表达式中的 extends
就是乘法分配律中的 *
。下面是表达这种类比的伪代码:
type Test = (AType + BType) * (SomeType ? 'yes' : 'no')
= AType * (SomeType ? 'yes' : 'no') + BType * (SomeType ? 'yes' : 'no')
另外,还有一个很重要的特性是,当联合类型的泛型形参的出现在三元表达式中的真值或者假值分支语句中,它指代的是正在遍历的联合类型的成员元素。在编程世界里面,利用联合类型的这个特性,我们可以遍历联合类型的所有成员类型。比如,ts 内置的 utility Exclude<T,U>
就是利用这种特性所实现的:
type MyExclude<T,U>= T extends U ? never : T; // 第二个条件分支语句中, T 指代的是正在遍历的成员元素
type Test = MyExclude<'a'|'b'|'c', 'a'> // 'b'|'c'
在上面的实现中,在你将类型实参代入到三元表达式中,对于第二个条件分支的T
记得要理解为'a'|'b'|'c'
的各个成员元素,而不是理解为完整的联合类型。
有时候,联合类型的这种分配律不是我们想要的。那么,我们该怎么消除这种特性呢?其实上面在讲「extends 与 never 」的时候也提到了。那就是,用方括号[]
包住 extends
前后的两个类型参数。此时,两个条件分支里面的联合类型参数在实例化时候的值将会跟 extends
子句里面的是一样的。
// 具有分配律的写法
type ToArray<Type> = Type extends any ? Type[] : never; //
type StrArrOrNumArr = ToArray<string | number>; // 结果是:`string[] | number[]`
// 消除分配律的写法
type ToArrayNonDist<Type> = [Type] extends [any] ? Type[] : never;
type StrArrOrNumArr2 = ToArray<string | number>; // 结果是:`(string | number)[]`
也许你会觉得 string[] | number[]
跟 (string | number)[]
是一样的,我只能说:“客官,要不您再仔细瞧瞧?”。
extends 判断类型严格相等
在 typeScript 的类型编程世界里面,很多时候我们需要判断两个类型是否是一模一样的,即这里所说的「严格相等」。如果让你去实现这个 utility 的话,你会怎么做呢?我相信,不少人会跟我一样,不假思索地写下了下面的答案:
type IsEquals<T,U>= T extends U ? U extends T ? true : false : false
这个答案似乎是逻辑正确的。因为,如果只有自己才可能既是自己的子类型也是自己的父类型。然后,我们用很多测试用例去测,似乎结果也都符合我们的预期。直到我们碰到下面的边缘用例:
type Test1= IsEquals<never,never> // 期待结果:true,实际结果: never
type Test2= IsEquals<1,any> // 期待结果:false,实际结果: boolean
type Test3= IsEquals<{readonly a: 1},{a:1}> // 期待结果:false,实际结果: true
没办法, typeScript 的类型系统有太多的违背常识的设计与实现了。如果还是沿用上面的思路,即使你把上面的特定用例修复好了,但是说不定还有其他的边缘用例躲在某个阴暗的角度等着你。所以,对于「如何判断两个 typeScript 类型是严格相等」的这个问题上,目前社区里面从 typeScript 实现源码角度上给出了一个终极答案:
type IsEquals<X, Y> =
(<T>() => (T extends X ? 1 : 2)) extends
(<T>() => (T extends Y ? 1 : 2))
? true
: false;
目前我还没理解这个终极答案为什么是行之有效的,但是从测试结果来看,它确实是 work 的,并且被大家所公认。所以,目前为止,对于这个实现只能是死记硬背了。
extends 与类型推导
type Test<A> = A extends SomeShape ? 第一个条件分支 : 第二支条件分支
当 typeScript 的三元表达式遇见类型推导infer SomeType
, 在语法上是有硬性要求的:
infer
只能出现在extends
子句中,并且只能出现在extends
关键字后面- 紧跟在
infer
后面所声明的类型形参只能在三元表达式的第一个条件分支(即,真值分支语句)中使用
除了语法上有硬性要求,我们也要正确理解 extends 遇见类型推导的语义。在这个上下文中,infer SomeType
更像是具有某种结构的类型的占位符。SomeShape
中可以通过 infer
来声明多个类型形参,它们与一些已知的类型值共同组成了一个代表具有如此形态的SomeShape
。而 A extends SomeShape
是我们开发者在表达:「tsc,请按照顾我所声明的这种结构去帮我推导得出各个泛型形参在运行时的值,以便供我进一步消费这些值」,而 tsc 会说:「好的,我尽我所能」。
「tsc 会尽我所能地去推导出具体的类型值」这句话的背后蕴含着不少的 typeScript 未在文档上交代的行为表现。比如,当类型形参与类型值共同出现在「数组」,「字符串」等可遍历的类型中,tsc 会产生类似于「子串/子数组匹配」的行为表现 - 也就是说,tsc 会以非贪婪匹配模式遍历整个数组/字符串进行子串/数组匹配,直到匹配到最小的子串/子数组为止。这个结果,就是我们类型推导的泛型形参在运行时的值。
举个例子,下面的代码是实现一个ReplaceOnce
类型 utility 代码:
type ReplaceOnce<
S extends string,
From extends string,
To extends string
> = From extends ""
? S
: S extends `${infer Left}${From}${infer Right}`
? `${Left}${To}${Right}`
: S
“”
type Test = Replace<"foobarbar", "bar", ""> // 结果是:“foobar”
tsc 在执行上面的这行代码「S extends ${infer Left}${From}${infer Right}
」的时候,背后做了一个从左到右的「子串匹配」行为,直到匹配到所传递进来的子串From
为止。这个时候,也是 resolve 出形参Left
和Right
具体值的时候。
以上示例很好的表达出我想要表达的「当extends
跟类型推导结合到一块所产生的一些微妙且未见诸于官方文档的行为表现」。在 typeScript 高级类型编程中,善于利用这一点能够帮助我们去解决很多「子串/子数组匹配」相关的问题。
总结
在 typeScript 在不同的上下文中,extends
有以下几个语义:
- 用于表达类型组合;
- 用于表达面向对象中「类」的继承
- 用于表达泛型的类型约束;
- 在条件类型(conditional type)中,充当类型表达式,用于求值。
最值得注意的是,extends
在条件类型中与其他几个特殊类型结合所产生的特殊语义。几个特殊类型是:
{}
any
never
联合类型
【推荐学习:javascript高级教程】
以上就是带你聊聊typeScript中的extends关键字的详细内容,更多请关注编程网其它相关文章!