返回顶部
首页 > 资讯 > 前端开发 > 其他 >带你聊聊typeScript中的extends关键字
  • 200
分享到

带你聊聊typeScript中的extends关键字

TypeScriptJavaScript 2023-05-14 22:05:02 200人浏览 八月长安
摘要

关于上面这张图,有几点可以单独拿出来强调一下:any 无处不在。它既是任何类型的子类型,也是任何类型的父类型,甚至可能是任意类型自己。所以,它可以赋值给任何类型;{} 充当 typescript 类型的时候,它是有特殊含义的 - 它对应是(

image.png

关于上面这张图,有几点可以单独拿出来强调一下:

  • any 无处不在。它既是任何类型的子类型,也是任何类型的父类型,甚至可能是任意类型自己。所以,它可以赋值给任何类型;
  • {} 充当 typescript 类型的时候,它是有特殊含义的 - 它对应是(Object.prototype.__proto__)=nulljs 原型链上的地位,它被视为所有的对象类型的基类。
  • 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,实际结果: 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 出形参LeftRight具体值的时候。

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

总结

在 typeScript 在不同的上下文中,extends 有以下几个语义:

  • 用于表达类型组合;
  • 用于表达面向对象中「类」的继承
  • 用于表达泛型的类型约束;
  • 在条件类型(conditional type)中,充当类型表达式,用于求值。

最值得注意的是,extends在条件类型中与其他几个特殊类型结合所产生的特殊语义。几个特殊类型是:

  • {}
  • any
  • never
  • 联合类型

【推荐学习javascript高级教程

以上就是带你聊聊typeScript中的extends关键字的详细内容,更多请关注编程网其它相关文章!

--结束END--

本文标题: 带你聊聊typeScript中的extends关键字

本文链接: https://lsjlt.com/news/207015.html(转载时请注明来源链接)

有问题或投稿请发送至: 邮箱/279061341@qq.com    QQ/279061341

猜你喜欢
  • 带你聊聊typeScript中的extends关键字
    关于上面这张图,有几点可以单独拿出来强调一下:any 无处不在。它既是任何类型的子类型,也是任何类型的父类型,甚至可能是任意类型自己。所以,它可以赋值给任何类型;{} 充当 typeScript 类型的时候,它是有特殊含义的 - 它对应是(...
    99+
    2023-05-14
    TypeScript JavaScript
  • typeScript中的extends关键字怎么使用
    本篇内容主要讲解“typeScript中的extends关键字怎么使用”,感兴趣的朋友不妨来看看。本文介绍的方法操作简单快捷,实用性强。下面就让小编来带大家学习“typeScript中的extends关键字怎么使用”吧!extends 是 ...
    99+
    2023-07-05
  • Typescript中extends关键字的基本使用
    目录前言基本使用泛型约束条件类型与高阶类型在高级类型中的应用参考文献总结前言 extends关键字在TS编程中出现的频率挺高的,而且不同场景下代表的含义不一样,特此总结一下: 表示继...
    99+
    2022-11-13
    ts extends关键字 ts extend
  • typeScript的extends关键字怎么使用
    本文小编为大家详细介绍“typeScript的extends关键字怎么使用”,内容详细,步骤清晰,细节处理妥当,希望这篇“typeScript的extends关键字怎么使用”文章能帮助大家解决疑惑,下面跟着小编的思路慢慢深入,一起来学习新知...
    99+
    2023-07-05
  • 深入聊聊C语言中的Const关键字
    目录前言01const简述02常量的应用常量作为函数的参数C++中应用加const03#define和const总结前言 const是一个C语言的关键字,它限定一个变量不允许被改变。...
    99+
    2024-04-02
  • 深入浅出聊一聊js中的'this'关键字
    目录前言什么是'this'关键字四种方式---1.调用函数的第一种方法是:将函数作为一种方法四种方式---2.调用函数的第二种方法是: 简单的调用函数,不将函数作为方...
    99+
    2024-04-02
  • 一文带你聊聊Nodejs中读写文件的操作
    操作文件是服务端一个基础的功能,也是做后端开发的必备能力之一。操作文件主要包括读和写。而这些功能 Nodejs 都已经提供了对应的方法。只要调用就行了。创建文件夹同步方法const fs = require('fs') fs...
    99+
    2022-11-22
    nodejs node
  • 详细聊聊TypeScript中unknown与any的区别
    目录前言1. unknown vs any2. unknown 和 any 的心智模式3.总结总结前言 我们知道 any 类型的变量可以被赋给任何值。 let myVar: a...
    99+
    2024-04-02
  • 聊聊PyTorch中eval和no_grad的关系
    首先这两者有着本质上区别 model.eval()是用来告知model内的各个layer采取eval模式工作。这个操作主要是应对诸如dropout和batchnorm这些在训练模式下...
    99+
    2024-04-02
  • 聊聊Python中关于a=[[]]*3的反思
    Python 关于a=[[]]*3的反思 之前用python做了一个关于交通大数据的项目,由于之前比较赶进度,故现在会陆续更新对项目代码的一些反思。 由此可以看出,a[0],a[1],a[2]指向的是同一个元素...
    99+
    2022-06-02
    Python a=[[]]*3
  • 带你了解Java中Static关键字的用法
    目录Java中Static关键字的一些用法详解1. Static 修饰类属性,因为静态成员变量可以通过类名+属性名调用,非静态成员变量不能通过类名+属性名调用;2. Static 修...
    99+
    2024-04-02
  • 聊一聊关于php源码中refcount的疑问
    在浏览PHP源码的时候,在众多的*.stub.php中,发现了这样的注释,@refcount 1。 通过翻看build/gen_stub.php源码,发现了在解析*.stub.php...
    99+
    2022-11-13
    php源码refcount php @refcount 1
  • 聊聊php截取中文字符串的问题
    PHP是一款广泛使用的编程语言,在开发网站与应用程序上有着广泛的应用。在PHP开发中,截取字符串是常见的需求。如果要截取中文字符串,需要一些特殊的处理。在PHP中,字符串处理函数常常用到,如substr、mb_substr,而且它们都可以用...
    99+
    2023-05-14
  • 聊聊jquery中隐藏表单字段的方法
    jQuery是一种广泛使用的JavaScript库,它被用来简化常见的客户端脚本任务。其中一个常见的任务是隐藏表单字段。这篇文章将介绍如何使用jQuery来隐藏表单字段。首先,要隐藏一个表单字段,需要使用CSS中的"display...
    99+
    2023-05-14
  • 聊聊javascript中常见的一些转义字符
    JavaScript是一种基于文本的编程语言,因此它需要一种机制来处理特殊字符。这些特殊字符可以是控制字符,例如换行符和制表符,或者是一些需要转义的字符,例如引号和反斜杠。在JavaScript中,使用反斜杠(\)来指示特殊字符。这被称为转...
    99+
    2023-05-14
  • 聊聊Golang中的转码系统及其相关技术
    近年来,随着人民生活水平的不断提高和互联网技术的不断发展,多语言环境下的编程和转码成为一种趋势。Go语言作为一种开源的、跨平台的编程语言,受到了越来越多的开发者的青睐。在Golang中,转码系统的实现是一个极具挑战性的任务。本文主要介绍Go...
    99+
    2023-05-14
  • 深入理解typescript中的infer关键字的使用
    目录infer案例:加深理解参考infer 这个关键字,整理记录一下,避免后面忘记了。有点难以理解呢。 infer infer 是在 typescript 2.8中新增的关键字。 ...
    99+
    2024-04-02
  • Java面向对象关键字extends继承的深入讲解
    目录一、问题引出二、继承extends2.1继承的用法2.2基本语法2.3继承的好处2.4继承性总结一、 问题引出 面向对象的编程思想使得代码中创建的类更加具体,他们都有各自的属性...
    99+
    2024-04-02
  • Python 聊聊socket中的listen()参数(数字)到底代表什么
    疑问 在调用socket的时候,我们会使用到listen()函数,里面有个参数叫backlog, 例如:socket.listen(5). 那么这个数字5到底代表什么意思呢?网上有...
    99+
    2024-04-02
  • 一篇文章带你了解C语言中volatile关键字
    目录C语言中volatile关键字总结C语言中volatile关键字 volatile关键字是C语言中非常冷门的关键字,因为用到这个关键字的场景并不多。 当不用这个关键字的时候,CP...
    99+
    2024-04-02
软考高级职称资格查询
编程网,编程工程师的家园,是目前国内优秀的开源技术社区之一,形成了由开源软件库、代码分享、资讯、协作翻译、讨论区和博客等几大频道内容,为IT开发者提供了一个发现、使用、并交流开源技术的平台。
  • 官方手机版

  • 微信公众号

  • 商务合作