extends
is a keyword in typeScript. In the type programming world of typeScript, the role it plays is really important, so we have to pay attention to it and learn it in depth. In my opinion, mastering it is a stepping stone into the world of advanced typeScript type programming. However, the reality is that it has very different semantics in different contexts, with different specifics. If this is not sorted out in depth, it can cause a lot of confusion for developers. Combing it and learning it in depth, and finally mastering it, this is my original intention of writing this article.
Let’s get straight to the point. In typeScript, in different contexts, extends
has the following semantics . Different semantics have different uses:
extends
can be used in combination with interface
for Expression type combination.
Example 1-1
interface ChildComponentProps { onChange: (val: string)=> void } interface ParentComponentProps extends ChildComponentProps { value: string }
In the react component development model, there is a bottom-up construction model - we tend to put all the lowest-level components first The props
of the subcomponent is built, and finally the props
of the container component
(responsible for promoting public state, aggregating and distributing props) is defined. At this time, inferface's extends
can express this semantic requirement - a combination of types (aggregating the props
of all subcomponents into one).
Of course, the extends
clause of interface
can be followed by multiple combined objects, with commas between multiple combined objects,
separated. For example, ParentComponentProps
props
that combines multiple subcomponents:
Example 1-2
interface ChildComponentProps { onChange: (val: string)=> void } interface ChildComponentProps2 { onReset: (value: string)=> void } interface ParentComponentProps extends ChildComponentProps, ChildComponentProps2 { value: string }
Note that what is pointed out above is "Multiple combined objects", this also includes Class
. Yes, it is the "class" in the general oriented concept. In other words, the following code is also legal:
Example 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 }
The reason why this is also legal is that everything stems from one feature: In typeScript, a class variable is both a "value" and a "type". In the context of interface extends class
, it is obvious that class is a "type" semantics. An interface extends
Another class can be understood as an interface that discards all the implementation code of this class and only combines it with the "type shape" of this class. In the above example code, from the perspective of type shape, SomeClass
is equivalent to the following interface:
Example 1-4
interface SomeClass { name: string updateName: (name:string)=> void }
Okay, the above is the semantics of the "type combination" of the extends
keyword. Things started to take a turn.
If an interface A inherits a class B, then this interface A can still be inherited (or combined) by other interfaces. However, if a class wants implements
this interface A, then this class can only be class B itself or a subclass of class B.
Example 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 } }
To solve this problem, class DropDownControl
must inherit Control
class or# Subclass of ##Control class:
Example 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 keyword Another semantics of - "inheritance". When
extends is used between typeScript classes, its exact semantics are the semantics of the "extends" keyword in object-oriented ES6.
AClass extends BClass should no longer be interpreted as "a combination of types" but as "AClass inherits BClass" and "AClass is a subclass of the parent class BClass" in object-oriented programming. At the same time, it is worth pointing out that the
extends keyword at this time lives in the "value world" and follows the same semantics as the
extends keyword in ES6. The more obvious point is that
extends in ts cannot inherit multiple parent classes at the same time. For example, the following code will report an error:
Example 1-7
class A {} class B {} // 报错: Classes can only extend a single class.(1174) class C extends A,B { }
extends with "inheritance" semantics More behavioral features The explanation already belongs to the category of object-oriented programming paradigm, so we will not discuss it in depth here. Interested students can learn about it by themselves.
extends keyword combined with
interface and
class:
used to express generic type constraints 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@4.9.4 上的运行结果。以上面这个心智模型(子集类型能赋值给父集类型,反之则不然)来看示例的运行结果,我们会有下面的直觉认知偏差:
Inany
is the parent set of number
. Why can it be assigned to a value of type number
? In number | string
should be the parent set of number
, so it cannot be assigned to a value of type number
. In number & string
should be the parent set of number
. Logically speaking, an error should be reported here, but why not? In {a:1}
is a subset of {a:1,b:2}
. Logically speaking, it can be assigned to {a:1,b:2}
type value, why is an error reported? {name: 'Uncle Shark'}
is a subset of SubClass
. Logically speaking, it can be assigned to SubClass
type value, why is an error reported? In BaseClass
is a subset of SubClass
. Logically speaking, it can be assigned to a value of type SubClass
. Why? Will an error be reported? After repeated verification and information review, the correct understanding is as follows:
any
is a subset of any type, and Any type of parent set. Here typeScript handles it in a loose direction, that is, it means a subset of number
; the reason why number | string
cannot be assigned to number
, not because number | string
is the parent set of number
, but because the union type encounters the "distributive law" result of the extends
keyword . That is because the result of number|string extends number
is equal to the result of (number extend number) | (string extends number)
. Obviously, the value of (number string extends number
is false
, so the entire type constraint is not satisfied; the type of the subtype The set type extends parent set type = true
to understand the mental model. Instead, we must adopt the parent set type extends subset type = true
. At the same time, when there is an explicit literal in the subset type When the key-value pair is used, it must also be included in the parent set type. Otherwise, it cannot be assigned to the subset type. number & string
should be regarded as types of object types , follow the rule above. Based on the correct understanding above, we might as well revise our mental model:
AType extends BType
, if AType
is BType
subtype, then we will say that AType
satisfies the type constraints we declared; Note: 1)
A -> B
means "A is the parent type of B, and B is the subtype of A"; 2 ) When the strictNullChecks compilation flag is turned on,undefined
,void
andnull
will not become a layer of the typeScript type system because they cannot be assigned to other types. of.
Regarding the above picture, there are a few points that can be highlighted separately:
any
everywhere. It is both a subtype of any type and a supertype of any type, and may even be any type itself. Therefore, it can be assigned to any type; {}
has a special meaning when used as a typeScript type - it corresponds to (Object.prototype.__proto__)= null
's position on the js prototype chain, it is regarded as the base class of all object types. The subtype of the literal form of array
is tuple
, and the subtype of the literal form of function
is function expression type
. tuple
and Function expression types
are included in Literal types
. Now we use this new mental model to understand Example 2-1 Where the error is reported:
type Test1_1 = UselessType<number|string>
之所以报错,是因为在类型约束中,如果 extends
前面的类型是联合类型,那么要想满足类型约束,则联合类型的每一个成员都必须满足类型约束才行。这就是所谓的「联合类型的分配律」。显然,string extends number
是不成立的,所以整个联合类型就不满足类型约束;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 通过类型约束的检验;众所周知,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,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)[]
是一样的,我只能说:“客官,要不您再仔细瞧瞧?”。
在 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高级教程】
The above is the detailed content of Let's talk to you about the extends keyword in typeScript. For more information, please follow other related articles on the PHP Chinese website!