Rumah > Artikel > hujung hadapan web > Mari berbincang dengan anda tentang kata kunci lanjutan dalam typeScript
extends
ialah kata kunci dalam typeScript. Dalam dunia pengaturcaraan jenis typeScript, peranan yang dimainkannya sangat penting, jadi kita perlu memberi perhatian kepadanya dan mempelajarinya secara mendalam. Pada pendapat saya, menguasainya adalah batu loncatan ke dalam dunia pengaturcaraan jenis typeScript lanjutan. Walau bagaimanapun, realitinya ialah ia mempunyai semantik yang sangat berbeza dalam konteks yang berbeza, dengan spesifikasi yang berbeza. Jika perkara ini tidak diselesaikan secara mendalam, ia boleh menyebabkan banyak kekeliruan bagi pembangun. Menyikatnya, mempelajarinya secara mendalam, dan akhirnya menguasainya, ini adalah niat asal saya menulis artikel ini.
Mari kita terus kepada intipati Dalam typeScript, dalam konteks yang berbeza, extends
mempunyai semantik berikut. Semantik yang berbeza mempunyai kegunaan yang berbeza:
untuk menyatakan komposisi jenis. extends
interface
Dalam model pembangunan komponen tindak balas, terdapat model pembinaan dari bawah - kami cenderung untuk meletakkan semua komponen terkini terlebih dahulu Subkomponen asas
interface ChildComponentProps { onChange: (val: string)=> void } interface ParentComponentProps extends ChildComponentProps { value: string }dibina, dan akhirnya
(bertanggungjawab untuk mempromosikan keadaan awam, mengagregat dan mengedarkan prop) ditakrifkan. Pada masa ini, props
inferface boleh menyatakan keperluan semantik ini - gabungan jenis (mengagregatkan semua sub-komponen' container component
menjadi satu bahagian). props
extends
Sudah tentu, klausa props
berbilang objek gabungan interface
, dipisahkan dengan koma extends
. Contohnya, menggabungkan berbilang subkomponen : ,
ParentComponentProps
props
Contoh 1-2
Perhatikan bahawa perkara di atas merujuk kepada "berbilang objek gabungan" ,
juga disertakan di sini. Ya, ia adalah "kelas" dalam konsep berorientasikan umum. Dalam erti kata lain, kod berikut juga sah:interface ChildComponentProps { onChange: (val: string)=> void } interface ChildComponentProps2 { onReset: (value: string)=> void } interface ParentComponentProps extends ChildComponentProps, ChildComponentProps2 { value: string }
Class
Contoh 1-3
Sebab mengapa ini juga sah ialah
semuanya datang daripada Ciri: Dalam typeScript, pembolehubah kelas ialah "nilai" dan "jenis"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 }. Dalam konteks
, jelas sekali bahawa kelas ialah makna semantik "jenis". Antara muka dan kelas lain boleh difahami sebagai antara muka yang membuang semua kod pelaksanaan kelas ini dan hanya menggabungkannya dengan "bentuk jenis" kelas ini. Dalam kod sampel di atas, dari perspektif bentuk jenis, adalah bersamaan dengan antara muka berikut: interface extends class
extends
SomeClass
Contoh 1-4
Baiklah , di atas ialah semantik "kombinasi jenis" kata kunci
. Perkara mula bertukar.interface SomeClass { name: string updateName: (name:string)=> void }
Jika antara muka A mewarisi kelas B, maka antara muka A ini masih boleh diwarisi (atau digabungkan) oleh antara muka lain. Walau bagaimanapun, jika kelas mahu extends
antara muka A ini, maka kelas ini hanya boleh menjadi kelas B itu sendiri atau subkelas kelas B.
implements
Contoh 1-5
Untuk menyelesaikan masalah ini,
mesti mewarisi kelasclass 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 } }atau subkelas
kelas: class DropDownControl
Control
Control
Contoh 1-6
Kod contoh di atas memaparkan semantik lain bagi kata kunci
- "warisan". Apabilaclass 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(); // Okdigunakan antara kelas typeScript, semantik tepatnya ialah semantik kata kunci "lanjutkan" dalam ES6 berorientasikan objek.
Ia seharusnya tidak lagi ditafsirkan sebagai "gabungan jenis" tetapi sebagai "AClass mewarisi BClass" dan "AClass ialah subkelas bagi kelas induk BClass" dalam pengaturcaraan berorientasikan objek. Pada masa yang sama, perlu dinyatakan bahawa kata kunci extends
pada masa ini hidup dalam "dunia nilai" dan mengikut semantik yang sama seperti kata kunci extends
dalam ES6. Perkara yang lebih jelas ialah AClass extends BClass
dalam ts tidak boleh mewarisi berbilang kelas induk pada masa yang sama. Sebagai contoh, kod berikut akan melaporkan ralat: extends
extends
extends
Contoh 1-7
Penjelasan lebih banyak ciri tingkah laku
dengan semantik "warisan" Ia sudah tergolong dalam kategori paradigma pengaturcaraan berorientasikan objek, jadi kami tidak akan membincangkannya secara mendalam di sini Pelajar yang berminat boleh mempelajarinya sendiri.class A {} class B {} // 报错: Classes can only extend a single class.(1174) class C extends A,B { }
Pada ketika ini, kami memahami dua semantik berbeza yang dinyatakan oleh kata kunci extends
digabungkan dengan
: extends
interface
class
gabungan jenis
更准确地说,这一节是要讨论 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; </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@4.9.4 上的运行结果。以上面这个心智模型(子集类型能赋值给父集类型,反之则不然)来看示例的运行结果,我们会有下面的直觉认知偏差:
any
ialah set induk number
Mengapakah ia boleh diberikan kepada nilai jenis number
? number | string
hendaklah set induk number
, jadi ia tidak boleh diberikan kepada nilai jenis number
. number & string
sepatutnya menjadi set induk number
Secara logiknya, ralat harus dilaporkan di sini, tetapi mengapa tidak? {a:1}
ialah subset daripada {a:1,b:2}
Secara logiknya, ia boleh ditetapkan kepada nilai jenis {a:1,b:2}
Mengapa ralat dilaporkan? {name: '鲨叔'}
ialah subset daripada SubClass
Secara logiknya, ia boleh ditetapkan kepada nilai jenis SubClass
Mengapa ralat dilaporkan? BaseClass
ialah subset daripada SubClass
Secara logiknya, ia boleh ditetapkan kepada nilai jenis SubClass
Mengapa ralat dilaporkan? Selepas pengesahan berulang dan semakan maklumat, pemahaman yang betul adalah seperti berikut:
any
ialah subset dari sebarang jenis dan juga merupakan induk daripada sebarang jenis set. Di sini typeScript mengendalikannya dalam arah yang longgar, iaitu, ia mengambil makna subset number
; sebab mengapa number | string
tidak boleh ditetapkan kepada number
bukan kerana number | string
ialah set induk number
, tetapi merupakan hasil daripada "undang-undang pengedaran" yang dihasilkan oleh jenis kesatuan yang menemui kata kunci extends
. Itu kerana hasil number|string extends number
sama dengan hasil (number extend number) | (string extends number)
. Jelas sekali, nilai (number string extends number
ialah false
, jadi keseluruhan kekangan jenis tidak berpuas hati; jenis objek 子集类型 extends 父集类型 = true
. Sebaliknya, gunakan 父集类型 extends 子集类型 = true
. Pada masa yang sama, apabila terdapat pasangan nilai kunci literal yang eksplisit dalam jenis subset, ia juga mesti terdapat dalam jenis superset. Jika tidak, ia tidak boleh diserahkan kepada jenis subset. number & string
hendaklah dianggap sebagai jenis jenis objek, mengikut peraturan di atas. Berdasarkan pemahaman yang betul di atas, kita juga boleh menyemak semula model mental kita:
AType extends BType
, jika AType
ialah subjenis BType
, maka kami akan mengatakan bahawa AType
memenuhi jenis yang kami isytiharkan Terkandas; >Nota: 1)Mengenai gambar di atas, terdapat beberapa perkara yang boleh diserlahkan secara berasingan:Ia bermaksud "A ialah jenis induk B, dan B ialah subjenis A"; 2) Selepas bendera kompilasi strictNullChecks dihidupkan,
A -> B
,undefined
danvoid
tidak akan menjadi lapisan sistem jenis TypeScript kerana ia tidak boleh ditugaskan kepada jenis lain.null
any
{}
dalam rantai prototaip js, dan ia adalah dianggap sebagai semua Kelas asas jenis objek. Subjenis bagi bentuk literal (Object.prototype.__proto__)=null
array
dan subjenis bentuk literal tuple
ialah function
. Kedua-dua 函数表达式类型
dan tuple
disertakan dalam 函数表达式类型
. 字面量类型
Contoh 2-1 Di mana ralat dilaporkan:
type Test1_1 = UselessType<number></number>
之所以报错,是因为在类型约束中,如果 extends
前面的类型是联合类型,那么要想满足类型约束,则联合类型的每一个成员都必须满足类型约束才行。这就是所谓的「联合类型的分配律」。显然,string extends number
是不成立的,所以整个联合类型就不满足类型约束;type Test2_1 = UselessType2
之所以报错,是因为{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
和 type Test3_2 = UselessType3<baseclass> </baseclass>
报错的原因也是因为因为缺少了相应的属性/方法,所以,它们都不是SubClass
的子类型。到这里,我们算是剖析完毕。下面总结一下。
extends
紧跟在泛型形参后面时,它是在表达「类型约束」的语义;AType extends BType
中,只有 AType
是 BType
的子类型,ts 通过类型约束的检验;众所周知,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% 的特性表现。
我们开门见山地问吧:“请说出下面代码的运行结果。”
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
有什么好讲得嘛。你上面不是说了「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
在 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
时所产生的分配律」。
上面也提到了,在 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></t>
就是利用这种特性所实现的:
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)[]
是一样的,我只能说:“客官,要不您再仔细瞧瞧?”。
在 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 的,并且被大家所公认。所以,目前为止,对于这个实现只能是死记硬背了。
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
有以下几个语义:
最值得注意的是,extends
在条件类型中与其他几个特殊类型结合所产生的特殊语义。几个特殊类型是:
{}
any
never
联合类型
【推荐学习:javascript高级教程】
Atas ialah kandungan terperinci Mari berbincang dengan anda tentang kata kunci lanjutan dalam typeScript. Untuk maklumat lanjut, sila ikut artikel berkaitan lain di laman web China PHP!