Home >Web Front-end >JS Tutorial >Let's talk to you about the extends keyword in typeScript

Let's talk to you about the extends keyword in typeScript

青灯夜游
青灯夜游forward
2023-02-13 19:39:142792browse

Let's talk to you about the extends keyword in typeScript

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.

Several semantics of extends

Let’s get straight to the point. In typeScript, in different contexts, extends has the following semantics . Different semantics have different uses:

  • is used to express type combinations;
  • is used to express the inheritance of "class" in object-oriented
  • is used to express generic Type constraints of type;
  • in conditional type (conditional type), acts as a type expression for evaluation.

extends and type combination/class inheritance

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, ParentComponentPropsprops 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

The above example code pulls out the

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 {

}

About

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.

At this point, we understand the two different semantics expressed by the

extends keyword combined with interface and class:

    Composition of types
  • "Inheritance of classes" in object-oriented concepts
Next, let's look at the

used to express generic type constraints 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;   </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: &#39;鲨叔&#39;}> // 这里会报错吗?
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 的运行结果:

Lets talk to you about the extends keyword in typeScript

实际上,上面的那个心智模型是无法匹配到以上示例在 typeScript@4.9.4 上的运行结果。以上面这个心智模型(子集类型能赋值给父集类型,反之则不然)来看示例的运行结果,我们会有下面的直觉认知偏差:

In
  • case 1, any is the parent set of number. Why can it be assigned to a value of type number? In
  • case 1, number | string should be the parent set of number, so it cannot be assigned to a value of type number. In
  • case 1, number & string should be the parent set of number. Logically speaking, an error should be reported here, but why not? In
  • case 2, {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?
  • In case 3, I feel that {name: 'Uncle Shark'} is a subset of SubClass. Logically speaking, it can be assigned to SubClass type value, why is an error reported? In
  • case 3, I feel that 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:

  • In case 1, 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
  • object type cannot use 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:

  • We should use "parent type" and " "Subtype" concept to understand the rules behind satisfying type constraints;
  • In the type constraint AType extends BType, if AType is BType subtype, then we will say that AType satisfies the type constraints we declared;
  • Judge the two types according to the following "ts type hierarchy diagram" Parent-child type relationship:

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 and null will not become a layer of the typeScript type system because they cannot be assigned to other types. of.

Lets talk to you about the extends keyword in typeScript

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></number> 之所以报错,是因为在类型约束中,如果 extends前面的类型是联合类型,那么要想满足类型约束,则联合类型的每一个成员都必须满足类型约束才行。这就是所谓的「联合类型的分配律」。显然,string extends number 是不成立的,所以整个联合类型就不满足类型约束;
  • 对于对象类型的类型 - 即强调由属性和方法所组成的集合类型,我们需要先用面向对象的概念来确定两个类型中,谁是子类,谁是父类。这里的判断方法是 - 如果 A 类型相比 B 类型多出了一些属性/方法的话(这也同时意味着 B 类型拥有的属性或者方法,A 类型也必须要有),那么 A 类型就是父类,B 类型就是子类。然后,我们再转换到子类型和父类型的概念上来 - 父类就是「父类型」,子类就是「子类型」。
    • 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 = UselessType3type Test3_2 = UselessType3<baseclass> </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 ? &#39;yes&#39; : &#39;no&#39;
          =  (AType extends SomeType ? &#39;yes&#39; : &#39;no&#39;) | (BType extends SomeType ? &#39;yes&#39; : &#39;no&#39;)

我们再来看看「乘法分配律」:(a+b)*c = a*c + b*c。对比一下,我们就是知道,三元表达式中的 |就是乘法分配律中的 +, 三元表达式中的 extends 就是乘法分配律中的 *。下面是表达这种类比的伪代码:

type Test = (AType + BType) * (SomeType ? &#39;yes&#39; : &#39;no&#39;)
          =  AType * (SomeType ? &#39;yes&#39; : &#39;no&#39;) + BType * (SomeType ? &#39;yes&#39; : &#39;no&#39;)

另外,还有一个很重要的特性是,当联合类型的泛型形参的出现在三元表达式中的真值或者假值分支语句中,它指代的是正在遍历的联合类型的成员元素。在编程世界里面,利用联合类型的这个特性,我们可以遍历联合类型的所有成员类型。比如,ts 内置的 utility Exclude<t></t> 就是利用这种特性所实现的:

type  MyExclude<T,U>= T extends U ? never :  T; // 第二个条件分支语句中, T 指代的是正在遍历的成员元素
type Test = MyExclude<&#39;a&#39;|&#39;b&#39;|&#39;c&#39;, &#39;a&#39;> // &#39;b&#39;|&#39;c&#39;

在上面的实现中,在你将类型实参代入到三元表达式中,对于第二个条件分支的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高级教程

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!

Statement:
This article is reproduced at:juejin.cn. If there is any infringement, please contact admin@php.cn delete