typeScript中的extends关键字怎么使用
本篇内容主要讲解“typeScript中的extends关键字怎么使用”,感兴趣的朋友不妨来看看。本文介绍的方法操作简单快捷,实用性强。下面就让小编来带大家学习“typeScript中的extends关键字怎么使用”吧!
贞丰网站建设公司创新互联公司,贞丰网站设计制作,有大型网站制作公司丰富经验。已为贞丰1000+提供企业网站建设服务。企业网站搭建\成都外贸网站建设要多少钱,请找那个售后服务好的贞丰做网站的公司定做!
extends
是 typeScript 中的关键字。在 typeScript 的类型编程世界里面,它所扮演的角色实在是太重要了,所以,我们不得不需要重视它,深入学习它。在我看来,掌握它就是进入高级 typeScript 类型编程世界的敲门砖。但是,现实是,它在不同的上下文中,具体不同的,相差很大的语义。如果没有深入地对此进行梳理,它会给开发者带来很大的困惑。
extends 的几个语义
让我们开门见山地说吧,在 typeScript 在不同的上下文中,extends
有以下几个语义。不同语义即有不同的用途:
用于表达类型组合;
用于表达面向对象中「类」的继承
用于表达泛型的类型约束;
在条件类型(conditional type)中,充当类型表达式,用于求值。
extends 与 类型组合/类继承
extends
可以跟 interface
结合起来使用,用于表达类型组合。
示例 1-1
interface ChildComponentProps {
onChange: (val: string)=> void
}
interface ParentComponentProps extends ChildComponentProps {
value: string
}
在 react 组件化开发模式中,存在一种自底向上的构建模式 - 我们往往会先把所有最底层的子组件的 props
构建好,最后才定义 container component
(负责提升公共 state,聚合和分发 props) 的 props
。此时,inferface 的 extends
正好能表达这种语义需求 - 类型的组合(将所有子组件的 props
聚合到一块)。
当然,interface
的 extends
从句是可以跟着多个组合对象,多个组合对象之间用逗号,
隔开。比如ParentComponentProps
组合多个子组件的 props
:
示例 1-2
interface ChildComponentProps {
onChange: (val: string)=> void
}
interface ChildComponentProps2 {
onReset: (value: string)=> void
}
interface ParentComponentProps extends ChildComponentProps, ChildComponentProps2 {
value: string
}
注意,上面指出的是「多个组合对象」,这里也包括了Class
。对,就是普通面向概念中的「类」。也就是说,下面的代码也是合法的:
示例 1-3
interface ChildComponentProps {
onChange: (val: string)=> void
}
interface ChildComponentProps2 {
onReset: (value: string)=> void
}
class SomeClass {
private name!: string // 变量声明时,变量名跟着一个感叹号`!`,这是「赋值断言」的语法
updateName(name:string){
this.name = name || ''
}
}
interface ParentComponentProps extends
ChildComponentProps,
ChildComponentProps2,
SomeClass {
value: string
}
之所以这也是合法的,一切源于一个特性:在 typeScript 中,一个 class 变量既是「值」也是「类型」。在interface extends class
的上下文中,显然是取 class 是「类型」的语义。一个 interface extends
另外一个 class,可以理解为 interface 抛弃这个 class 的所有实现代码,只是跟这个 class 的「类型 shape」 进行组合。还是上面的示例代码中,从类型 shape 的角度,SomeClass
就等同于下面的 interface:
示例 1-4
interface SomeClass {
name: string
updateName: (name:string)=> void
}
好了,以上就是 extends
关键字的「类型组合」的语义。事情开始发生了转折。
如果某个 interface A 继承了某个 class B,那么这个 interface A 还是能够被其他 interface 去继承(或者说组合)。但是,如果某个 class 想要 implements
这个 interface A,那么这个 class 只能是 class B 本身或者 class B 的子类。
示例 1-5
class Control {
private state: any;
constructor(intialValue: number){
if(intialValue > 10){
this.state = false
}else {
this.state = true
}
}
checkState(){
return this.state;
}
}
interface SelectableControl extends Control {
select(): void;
}
// 下面的代码会报错:Class 'DropDownControl' incorrectly implements interface
// 'SelectableControl'.
// Types have separate declarations of a private property 'state'.(2420)
class DropDownControl implements SelectableControl {
private state = false;
checkState(){
// do something
}
select(){
// do something
}
}
要想解决这个问题,class DropDownControl
必须要继承 Control
class 或者Control
class 的子类:
示例 1-6
class Control {
private state: any;
constructor(intialValue: number){
if(intialValue > 10){
this.state = false
}else {
this.state = true
}
}
checkState(){
return this.state;
}
}
interface SelectableControl extends Control {
select(): void;
}
// 下面的代码就不会报错,且能得到预期的运行结果
class DropDownControl extends Control implements SelectableControl {
// private state = false;
//checkState(){
// do something
//}
select(){
// do something
}
}
const dropDown = new DropDownControl(1);
dropDown.checkState(); // Ok
dropDown.select(); // Ok
上面这个示例代码扯出了 extends
关键字的另外一个语义 - 「继承」。当extends
用于 typeScript 的类之间,它的准确语义也就是 ES6 中面向对象中「extends」关键字的语义。AClass extends BClass
不再应该解读为「类型的组合」而是面向对象编程中的「AClass 继承 BClass」和「AClass 是父类 BClass 的子类」。与此同时,值得指出的是,此时的 extends
关键字是活在了「值的世界」, 遵循着 ES6 中 extends
关键字一样的语义。比较显著的一点就是,ts 中的 extends
也是不能在同一时间去继承多个父类的。比如,下面的代码就会报错:
示例 1-7
class A {}
class B {}
// 报错: Classes can only extend a single class.(1174)
class C extends A,B {
}
关于具有「继承」语义的 extends
更多行为特性的阐述已经属于面向对象编程范式的范畴了,这里就不深入讨论了,有兴趣的同学可以自行去了解。
至此,我们算是了解 extends
关键字跟 interface
和 class
结合起来所表达的两种不同的语义:
类型的组合
面向对象概念中「类的继承」
接下来,我们看看用于表达泛型类型约束的 extends
extends 与类型约束
更准确地说,这一节是要讨论 extends
跟泛型形参结合时候的「类型约束」语义。在更进一步讨论之前,我们不妨先复习一下,泛型形参声明的语法以及我们可以在哪些地方可以声明泛型形参。
具体的泛型形参声明语法是:
标识符后面用尖括号
<>
包住一个或者多个泛型形参多个泛型形参用
,
号隔开泛型新参的名字可以随意命名(我们见得最多就是使用单个英文字母
T
,U
之类的)。
在 typeScript 中,我们可以在以下地方去声明一个泛型形参。
在普通的函数声明中:
function dispatch(action: A): A {
// Do something
}const dispatch:
(action: A)=> A = (action)=> {
return action
}
// 或者
interface Store {
dispatch: (action: A)=> A
}interface Store
{
dispatch:(action: A)=> A
reducer: (state: S,action: A)=> S
}class GenericAdd
{
zeroValue!: AddableType;
add!: (x: AddableType, y: AddableType) => AddableType;
}
let myGenericNumber = new GenericNumber();
myGenericNumber.zeroValue = 0;
myGenericNumber.add = function (x, y) {
return x + y;
};type Dispatch
=(action:A)=> A
`泛型形参` extends `某个类型`
为了引出上面所说「满足」的理解难题,我们不妨先看看下面的示例的代码:
// case 1 type UselessType
= T; type Test1 = UselessType // 这里会报错吗? type Test1_1 = UselessType // 这里会报错吗? // case 2 type UselessType2 = T; type Test2 = UselessType2<{a:1, b:2, c:3}> // 这里会报错吗? type Test2_1 = UselessType2<{a:1}> // 这里会报错吗? type Test2_2 = UselessType2<{[key:string]: any}> // 这里会报错吗? type Test2_3 = {a:1, b:2} extends {[key:string]: any} ? true : false // case 3 class BaseClass { name!: string } class SubClass extends BaseClass{ sayHello!: (name: string)=> void } class SubClass2 extends SubClass{ logName!: ()=> void } type UselessType3 = T; type Test3 = UselessType3<{name: '鲨叔'}> // 这里会报错吗? type Test3_1 = UselessType3 // 这里会报错吗? type Test3_2 = UselessType3 // 这里会报错吗?
case 1 中,number | string
应该是number
的父集,所以,它不能赋值给number
类型的值。case 1 中,number & string
应该是number
的父集,按理说,这里应该报错,但是为什么却没有?case 2 中,{a:1}
是{a:1,b:2}
的子集,按理说,它能赋值给{a:1,b:2}
类型的值啊,为什么会报错?case 3 中,感觉{name: '鲨叔'}
是SubClass
的子集,按理说,它能赋值给SubClass
类型的值啊,为什么会报错?case 3 中,感觉BaseClass
是SubClass
的子集,按理说,它能赋值给SubClass
类型的值啊,为什么会报错?
{}
充当 typeScript 类型的时候,它是有特殊含义的 - 它对应是(Object.prototype.__proto__)=null
在 js 原型链上的地位,它被视为所有的对象类型的基类。array
的字面量形式的子类型就是tuple
,function
的字面量形式的子类型就是函数表达式类型
。tuple
和函数表达式类型
都被囊括到字面量类型
中去。
现在我们用这个新的心智模型去理解一下 示例 2-1报错的地方:
extends 与条件类型
众所周知,ts 中的条件类型就是 js 世界里面的「三元表达式」。只不过,相比值世界里面的三元表达式最终被计算出一个「值」,ts 的三元表达式最终计算出的是「类型」。下面,我们先来复习一下它的语法:
AType extends BType ? CType : DType
跟 js 的三元表达式支持嵌套一样,ts 的三元表达式也支持嵌套,即下面也是合法的语法:
AType extends BType ? (CType extends DType ? EType : FType) : (GType extends HType ? IType : JType)
幸运的是,我们可以复用「extends 与类型约束」上面所产出的心智模型。简而言之,如果 AType
是 BType
的子类型,那么代码执行就是进入第一个条件分支语句,否则就会进入第二个条件分支语句。
上面这句话再加上「ts 类型层级关系图」,我们几乎可以理解AType extends BType
99% 的语义。还剩下 1% 就是那些违背正常人直觉的特性表现。下面我们重点说说这 1% 的特性表现。
extends 与 {}
type Test = 1 extends {} ? true : false // 请问 `Test` 类型的值是什么?
type Test = unknown extends {} ? true : false // `Test` 类型的值是 `false`
Test2
类型的值是 false
,从而证明了unknown
是{}
的父类型。
extends 与 any
也许你会觉得,extends
与 any
有什么好讲得嘛。你上面不是说了「any
」既是所有类型的子类型,又是所有类型的父类型。所以,以下示例代码得到的类型一定是true
:
type Test = any extends number ? true : false
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
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
type IsNever
= T extends never ? true : false
然后,你信心满满地给泛型形参传递个never
去测试,你发现结果是never
,而不是true
或者false
:
type Test = IsNever
// Test 的值为 `never`, 而不是我们期待的 `true`
type Test = never extends never ? true : false // Test 的值为 `true`, 符合我们的预期
你会发现,这次的结果却是符合我们的预期的。此时,你脑海里面肯定有千万匹草泥马奔腾而过。是的,ts 类型系统中,某些行为就是那么的匪夷所思。
type IsNever
= [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')
type Test = (AType + BType) * (SomeType ? 'yes' : 'no') = AType * (SomeType ? 'yes' : 'no') + BType * (SomeType ? 'yes' : 'no')
type MyExclude
= T extends U ? never : T; // 第二个条件分支语句中, T 指代的是正在遍历的成员元素 type Test = MyExclude<'a'|'b'|'c', 'a'> // 'b'|'c'
在上面的实现中,在你将类型实参代入到三元表达式中,对于第二个条件分支的T
记得要理解为'a'|'b'|'c'
的各个成员元素,而不是理解为完整的联合类型。
// 具有分配律的写法 type ToArray
= Type extends any ? Type[] : never; // type StrArrOrNumArr = ToArray ; // 结果是:`string[] | number[]` // 消除分配律的写法 type ToArrayNonDist = [Type] extends [any] ? Type[] : never; type StrArrOrNumArr2 = ToArray ; // 结果是:`(string | number)[]`
也许你会觉得 string[] | number[]
跟 (string | number)[]
是一样的,我只能说:“客官,要不您再仔细瞧瞧?”。
extends 判断类型严格相等
type IsEquals
= T extends U ? U extends T ? true : false : false
这个答案似乎是逻辑正确的。因为,如果只有自己才可能既是自己的子类型也是自己的父类型。然后,我们用很多测试用例去测,似乎结果也都符合我们的预期。直到我们碰到下面的边缘用例:
type Test1= IsEquals
// 期待结果:true,实际结果: never type Test2= IsEquals<1,any> // 期待结果:false,实际结果: boolean type Test3= IsEquals<{readonly a: 1},{a:1}> // 期待结果:false,实际结果: true
type IsEquals
= ( () => (T extends X ? 1 : 2)) extends ( () => (T extends Y ? 1 : 2)) ? true : false;
目前我还没理解这个终极答案为什么是行之有效的,但是从测试结果来看,它确实是 work 的,并且被大家所公认。所以,目前为止,对于这个实现只能是死记硬背了。
extends 与类型推导
type Test
= A extends SomeShape ? 第一个条件分支 : 第二支条件分支
当 typeScript 的三元表达式遇见类型推导infer SomeType
, 在语法上是有硬性要求的:
举个例子,下面的代码是实现一个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”
到此,相信大家对“typeScript中的extends关键字怎么使用”有了更深的了解,不妨来实际操作一番吧!这里是创新互联网站,更多相关内容可以进入相关频道进行查询,关注我们,继续学习!
文章名称:typeScript中的extends关键字怎么使用
网站网址:http://azwzsj.com/article/ijoojj.html