typeScript中的extends关键字怎么运用
发布时间:2023-07-29 12:00:32 所属栏目:教程 来源:网络
导读: 本篇内容主要讲解“typeScript中的extends关键字怎么使用”,感兴趣的朋友不妨来看看。本文介绍的方法操作简单快捷,实用性强。下面就让小编来带大家学习“typeScript中
本篇内容主要讲解“typeScript中的extends关键字怎么使用”,感兴趣的朋友不妨来看看。本文介绍的方法操作简单快捷,实用性强。下面就让小编来带大家学习“typeScript中的extends关键字怎么使用”吧! 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<A>(action: A): A { // Do something } 在函数表达式形态的类型注解中: const dispatch: <A>(action: A)=> A = (action)=> { return action } // 或者 interface Store { dispatch: <A>(action: A)=> A } 在 interface 的声明中: interface Store<S> { dispatch: <A>(action: A)=> A reducer: <A>(state: S,action: A)=> S } 在 class 的声明中: class GenericAdd<AddableType> { zeroValue!: AddableType; add!: (x: AddableType, y: AddableType) => AddableType; } let myGenericNumber = new GenericNumber<number>(); myGenericNumber.zeroValue = 0; myGenericNumber.add = function (x, y) { return x + y; }; 在自定义类型声明中: type Dispatch<A>=(action:A)=> A 在类型推导中:typeScript // 此处,F 和 Rest 就是泛型形参 type GetFirstLetter<S> = S extends `${infer F extends `${number}`}${infer Rest}` ? F : S; 以上就是简单梳理后的可以产生泛型形参的地方,可能还有疏漏,但是这里就不深入发掘了。 下面重点来了 - 凡是有泛型形参的地方,我们都可以通过 extends 来表达类型约束。这里的类型约束展开说就是,泛型形参在实例化时传进来的类型实参必须要满足我们所声明的类型约束。到这里,问题就来了,我们该怎样来理解这里的「满足」呢?在深究此问题之前,我们来看看类型约束的语法: `泛型形参` extends `某个类型` 为了引出上面所说「满足」的理解难题,我们不妨先看看下面的示例的代码: 示例 2-1 // case 1 type UselessType<T extends number> = T; type Test1 = UselessType<any> // 这里会报错吗? type Test1_1 = UselessType<number|string> // 这里会报错吗? // case 2 type UselessType2<T extends {a:1, b:2}> = 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 extends SubClass> = T; type Test3 = UselessType3<{name: '鲨叔'}> // 这里会报错吗? type Test3_1 = UselessType3<SubClass> // 这里会报错吗? type Test3_2 = UselessType3<BaseClass> // 这里会报错吗? 不知道读者朋友们在没有把上述代码拷贝到 typeScript 的 playground 里面去验证之前你是否能全部猜中。如果能,证明你对 extends 在类型约束的语义上下文中的行为表现已经掌握的很清楚了。如果不能,请允许我为你娓娓道来。 相信有部分读者了解过 typeScript 的类型系统的设计策略。由于 js 是一门动态弱类型的脚本语言,再加上需要考虑 typeScript 与 js 的互操性和兼容性。所以, typeScript 类型系统被设计为一个「structural typing」系统(结构化类型系统)。所谓的结构化类型系统的一个显著的特点就是 - 具有某个类型 A 的值是否能够赋值给另外一个类型 B 的值的依据是,类型 A 的类型结构是否跟类型 B 的类型结构是否兼容。 而类型之间是否兼容看重的类型的结构而不是类型的名字。再说白一点,就是 B 类型有的属性和方法,你 A 类型也必须有。到这里,就很容易引出一个广为大众接受的,用于理解类型「可赋值性」行为的心智模型,即: 用集合的角度去看类型。故而这里有「父集」和 「子集」的概念,「父集」包含 「子集」; 在 typeScript 的类型系统中, 子集类型是可以赋值给父集类型。 在泛型形参实例化时,如果 extends 前面的类型是它后面的类型的子集,那么我们就说当前的实例化是「满足」我们所声明的类型约束的。 以下是 示例 2-1 的运行结果: typeScript中的extends关键字怎么使用 实际上,上面的那个心智模型是无法匹配到以上示例在 typeScript@4.9.4 上的运行结果。以上面这个心智模型(子集类型能赋值给父集类型,反之则不然)来看示例的运行结果,我们会有下面的直觉认知偏差: case 1 中,any 是 number 的父集,为什么它能赋值给 number 类型的值? 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类型的值啊,为什么会报错? 经过反复验证和查阅资料,正确的认知如下: case 1 中,any 是任何类型的子集,也是任何类型的父集。这里 typeScript 往宽松方向去处理,即取 number 的子集之意; number | string 之所以不能赋值给 number ,并不是因为 number | string 是 number 的父集,而是因为联合类型遇到 extends关键字所产生的「分配律」的结果。即是因为 number|string extends number的结果等于 (number extend number) | (string extends number)的结果。显然,(number string extends number的值是 false 的,所以,整个类型约束就不满足; 对象类型的类型不能采用 子集类型 extends 父集类型 = true的心智模型来理解。而是得采用 父集类型 extends 子集类型 = true。与此同时,当子集类型中有明确字面量 key-value 对的时候,父集类型中也必须需要有。否则的话,就是不可赋值给子集类型。 number & string 应该被视为对象类型的类型,遵循上面一条的规则。 基于上面的正确认知,我们不妨把我们的心智模型修正一下: 应该使用「父类型」和「子类型」的概念去理解满足类型约束背后所遵循的规则; 在类型约束 AType extends BType 中,如果 AType 是 BType的子类型,那么我们就会说 AType 是满足我们所声明的类型约束的; 根据下面的 「ts 类型层级关系图」来判断两种类型的父-子类型关系: 注:1)A -> B表示「A 是 B 的父类型,B 是 A 的子类型」;2)strictNullChecks 编译标志位打开后,undefined,void和null就不会成为 typeScript 类型系统的一层,因为它们是不能赋值给其他类型的。 typeScript中的extends关键字怎么使用 关于上面这张图,有几点可以单独拿出来强调一下: 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 (编辑:源码网) 【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容! |
推荐文章
站长推荐