我的编程空间,编程开发者的网络收藏夹
学习永远不晚

typeScript中的extends关键字怎么使用

短信预约 -IT技能 免费直播动态提醒
省份

北京

  • 北京
  • 上海
  • 天津
  • 重庆
  • 河北
  • 山东
  • 辽宁
  • 黑龙江
  • 吉林
  • 甘肃
  • 青海
  • 河南
  • 江苏
  • 湖北
  • 湖南
  • 江西
  • 浙江
  • 广东
  • 云南
  • 福建
  • 海南
  • 山西
  • 四川
  • 陕西
  • 贵州
  • 安徽
  • 广西
  • 内蒙
  • 西藏
  • 新疆
  • 宁夏
  • 兵团
手机号立即预约

请填写图片验证码后获取短信验证码

看不清楚,换张图片

免费获取短信验证码

typeScript中的extends关键字怎么使用

本篇内容主要讲解“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 聚合到一块)。

当然,interfaceextends 从句是可以跟着多个组合对象,多个组合对象之间用逗号,隔开。比如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 extendsChildComponentProps,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(); // OkdropDown.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 关键字跟 interfaceclass 结合起来所表达的两种不同的语义:

  • 类型的组合

  • 面向对象概念中「类的继承」

接下来,我们看看用于表达泛型类型约束的 extends

extends 与类型约束

更准确地说,这一节是要讨论 extends 跟泛型形参结合时候的「类型约束」语义。在更进一步讨论之前,我们不妨先复习一下,泛型形参声明的语法以及我们可以在哪些地方可以声明泛型形参。

具体的泛型形参声明语法是:

  • 标识符后面用尖括号<>包住一个或者多个泛型形参

  • 多个泛型形参用,号隔开

  • 泛型新参的名字可以随意命名(我们见得最多就是使用单个英文字母TU之类的)。

在 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 1type UselessType<T extends number> = T;type Test1 = UselessType<any> // 这里会报错吗?type Test1_1 = UselessType<number|string> // 这里会报错吗?// case 2type 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 3class 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 中,anynumber 的父集,为什么它能赋值给 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 中,感觉BaseClassSubClass 的子集,按理说,它能赋值给 SubClass类型的值啊,为什么会报错?

经过反复验证和查阅资料,正确的认知如下:

  • case 1 中,any 是任何类型的子集,也是任何类型的父集。这里 typeScript 往宽松方向去处理,即取 number 的子集之意;

  • number | string 之所以不能赋值给 number ,并不是因为 number | stringnumber 的父集,而是因为联合类型遇到 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 中,如果 ATypeBType的子类型,那么我们就会说 AType 是满足我们所声明的类型约束的;

  • 根据下面的 「ts 类型层级关系图」来判断两种类型的父-子类型关系:

注:1)A -> B表示「A 是 B 的父类型,B 是 A 的子类型」;2)strictNullChecks 编译标志位打开后,undefined,voidnull就不会成为 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 中,只有 ATypeBType 的子类型,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 与类型约束」上面所产出的心智模型。简而言之,如果 ATypeBType 的子类型,那么代码执行就是进入第一个条件分支语句,否则就会进入第二个条件分支语句。

上面这句话再加上「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

也许你会觉得,extendsany 有什么好讲得嘛。你上面不是说了「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,实际结果: nevertype  Test2= IsEquals<1,any> // 期待结果:false,实际结果: booleantype  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 出形参LeftRight具体值的时候。

以上示例很好的表达出我想要表达的「当extends 跟类型推导结合到一块所产生的一些微妙且未见诸于官方文档的行为表现」。在 typeScript 高级类型编程中,善于利用这一点能够帮助我们去解决很多「子串/子数组匹配」相关的问题。

到此,相信大家对“typeScript中的extends关键字怎么使用”有了更深的了解,不妨来实际操作一番吧!这里是编程网网站,更多相关内容可以进入相关频道进行查询,关注我们,继续学习!

免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

typeScript中的extends关键字怎么使用

下载Word文档到电脑,方便收藏和打印~

下载Word文档

猜你喜欢

typeScript中的extends关键字怎么使用

本篇内容主要讲解“typeScript中的extends关键字怎么使用”,感兴趣的朋友不妨来看看。本文介绍的方法操作简单快捷,实用性强。下面就让小编来带大家学习“typeScript中的extends关键字怎么使用”吧!extends 是
2023-07-05

typeScript的extends关键字怎么使用

本文小编为大家详细介绍“typeScript的extends关键字怎么使用”,内容详细,步骤清晰,细节处理妥当,希望这篇“typeScript的extends关键字怎么使用”文章能帮助大家解决疑惑,下面跟着小编的思路慢慢深入,一起来学习新知
2023-07-05

Typescript中extends关键字的基本使用

extends表示具体的泛型类型只能是object类型,某个变量如果能断言成object类型[变量asobject],那么这个变量的类型符合Textendsobject,下面这篇文章主要给大家介绍了关于Typescript中extends关键字基本使用的相关资料,需要的朋友可以参考下
2022-11-13

带你聊聊typeScript中的extends关键字

extends 是 typeScript 中的关键字。在 typeScript 的类型编程世界里面,它所扮演的角色实在是太重要了,所以,我们不得不需要重视它,深入学习它。
2023-05-14

LINQ中的关键字怎么使用

这篇文章主要讲解了“LINQ中的关键字怎么使用”,文中的讲解内容简单清晰,易于学习与理解,下面请大家跟着小编的思路慢慢深入,一起来研究和学习“LINQ中的关键字怎么使用”吧!什么是LINQLINQ是Language Integrated Q
2023-06-17

java中的volatile关键字怎么使用

本篇内容介绍了“java中的volatile关键字怎么使用”的有关知识,在实际案例的操作过程中,不少人都会遇到这样的困境,接下来就让小编带领大家学习一下如何处理这些情况吧!希望大家仔细阅读,能够学有所成!1.volatile实现可见性的原理
2023-06-25

java中的final关键字怎么使用

在Java中,final关键字可以用于修饰类、方法和变量。1. final修饰类:final修饰的类是不可被继承的,即该类不能有子类。例如:```javafinal class MyClass {// 类的内容}```2. final修饰方
2023-08-24

java中的super关键字怎么使用

在Java中,super是一个关键字,用于引用父类的成员变量、成员方法和构造方法。1. 引用父类的成员变量:可以使用super关键字来引用父类中的成员变量。例如,如果父类中有一个成员变量名为num,可以使用super.num来引用父类中的n
2023-08-08

Golang中的defer关键字怎么使用

在Golang中,defer关键字用于注册一个函数调用,该函数会在当前函数执行完成后被执行,无论函数是正常返回还是发生了panic。defer语句通常用于在函数执行结束后释放资源或执行一些清理操作。defer关键字的语法如下:func
Golang中的defer关键字怎么使用
2024-03-13

Java中transient关键字怎么使用

本篇内容主要讲解“Java中transient关键字怎么使用”,感兴趣的朋友不妨来看看。本文介绍的方法操作简单快捷,实用性强。下面就让小编来带大家学习“Java中transient关键字怎么使用”吧!一、概要介绍 对于transient
2023-07-06

C++中register关键字怎么使用

本篇内容介绍了“C++中register关键字怎么使用”的有关知识,在实际案例的操作过程中,不少人都会遇到这样的困境,接下来就让小编带领大家学习一下如何处理这些情况吧!希望大家仔细阅读,能够学有所成!register 简介:register
2023-07-05

java中this关键字怎么使用

这篇文章主要介绍了java中this关键字怎么使用的相关知识,内容详细易懂,操作简单快捷,具有一定借鉴价值,相信大家阅读完这篇java中this关键字怎么使用文章都会有所收获,下面我们一起来看看吧。this 的使用:修饰属性和方法,也可以理
2023-06-26

Java中 transient关键字怎么使用

本篇文章为大家展示了Java中 transient关键字怎么使用,内容简明扼要并且容易理解,绝对能使你眼前一亮,通过这篇文章的详细介绍希望你能有所收获。1. transient的作用及使用方法我们都知道一个对象只要实现了Serilizabl
2023-06-19

java中abstract关键字怎么使用

在Java中,abstract关键字主要用于定义抽象类和抽象方法。1. 抽象类的定义:使用abstract关键字修饰类,将其声明为抽象类。抽象类不能被实例化,只能被继承。抽象类可以包含普通方法和抽象方法。示例代码:```javaabstra
2023-09-23

Java中super关键字怎么使用

这篇文章主要讲解了“Java中super关键字怎么使用”,文中的讲解内容简单清晰,易于学习与理解,下面请大家跟着小编的思路慢慢深入,一起来研究和学习“Java中super关键字怎么使用”吧!supersuper是一个关键字,全部小写。sup
2023-07-02

C++中auto关键字怎么使用

今天小编给大家分享一下C++中auto关键字怎么使用的相关知识点,内容详细,逻辑清晰,相信大部分人都还太了解这方面的知识,所以分享这篇文章给大家参考一下,希望大家阅读完这篇文章后有所收获,下面我们一起来了解一下吧。前提引入1.类型名,在绝大
2023-07-06

Objective-C中的@Synchronized关键字怎么使用

这篇文章主要介绍“Objective-C中的@Synchronized关键字怎么使用”,在日常操作中,相信很多人在Objective-C中的@Synchronized关键字怎么使用问题上存在疑惑,小编查阅了各式资料,整理出简单好用的操作方法
2023-07-05

Python中nonlocal关键字与global关键字怎么用

小编给大家分享一下Python中nonlocal关键字与global关键字怎么用,相信大部分人都还不怎么了解,因此分享这篇文章给大家参考一下,希望大家阅读完这篇文章后大有收获,下面让我们一起去了解一下吧!python引用变量的顺序: 当前作
2023-06-29

SQL的Merge关键字怎么使用

这篇文章主要介绍“SQL的Merge关键字怎么使用”的相关知识,小编通过实际案例向大家展示操作过程,操作方法简单快捷,实用性强,希望这篇“SQL的Merge关键字怎么使用”文章能帮助大家解决问题。Merge关键字是一个神奇的DML关键字。它
2023-06-27

编程热搜

  • Python 学习之路 - Python
    一、安装Python34Windows在Python官网(https://www.python.org/downloads/)下载安装包并安装。Python的默认安装路径是:C:\Python34配置环境变量:【右键计算机】--》【属性】-
    Python 学习之路 - Python
  • chatgpt的中文全称是什么
    chatgpt的中文全称是生成型预训练变换模型。ChatGPT是什么ChatGPT是美国人工智能研究实验室OpenAI开发的一种全新聊天机器人模型,它能够通过学习和理解人类的语言来进行对话,还能根据聊天的上下文进行互动,并协助人类完成一系列
    chatgpt的中文全称是什么
  • C/C++中extern函数使用详解
  • C/C++可变参数的使用
    可变参数的使用方法远远不止以下几种,不过在C,C++中使用可变参数时要小心,在使用printf()等函数时传入的参数个数一定不能比前面的格式化字符串中的’%’符号个数少,否则会产生访问越界,运气不好的话还会导致程序崩溃
    C/C++可变参数的使用
  • css样式文件该放在哪里
  • php中数组下标必须是连续的吗
  • Python 3 教程
    Python 3 教程 Python 的 3.0 版本,常被称为 Python 3000,或简称 Py3k。相对于 Python 的早期版本,这是一个较大的升级。为了不带入过多的累赘,Python 3.0 在设计的时候没有考虑向下兼容。 Python
    Python 3 教程
  • Python pip包管理
    一、前言    在Python中, 安装第三方模块是通过 setuptools 这个工具完成的。 Python有两个封装了 setuptools的包管理工具: easy_install  和  pip , 目前官方推荐使用 pip。    
    Python pip包管理
  • ubuntu如何重新编译内核
  • 改善Java代码之慎用java动态编译

目录