Rumah  >  Artikel  >  hujung hadapan web  >  Mari berbincang dengan anda tentang kata kunci lanjutan dalam typeScript

Mari berbincang dengan anda tentang kata kunci lanjutan dalam typeScript

青灯夜游
青灯夜游ke hadapan
2023-02-13 19:39:142682semak imbas

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.

Beberapa semantik lanjutan

Mari kita terus kepada intipati Dalam typeScript, dalam konteks yang berbeza, extends mempunyai semantik berikut. Semantik yang berbeza mempunyai kegunaan yang berbeza:

  • digunakan untuk menyatakan gabungan jenis
  • digunakan untuk menyatakan warisan "kelas" dalam berorientasikan objek; digunakan untuk menyatakan kekangan Jenis generik jenis
  • dalam jenis bersyarat (jenis bersyarat), bertindak sebagai ungkapan jenis untuk penilaian.
melanjutkan dan menaip komposisi/warisan kelas

boleh digunakan dalam kombinasi dengan

untuk menyatakan komposisi jenis. extendsinterface

Contoh 1-1

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). propsextends Sudah tentu, klausa props

boleh diikuti dengan

berbilang objek gabungan interface, dipisahkan dengan koma extends. Contohnya, menggabungkan berbilang subkomponen : ,ParentComponentPropspropsContoh 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
}

ClassContoh 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 classextendsSomeClassContoh 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.

implementsContoh 1-5

Untuk menyelesaikan masalah ini,

mesti mewarisi kelas
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
  }
}
atau subkelas

kelas: class DropDownControlControlControlContoh 1-6

Kod contoh di atas memaparkan semantik lain bagi kata kunci

- "warisan". Apabila
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
digunakan 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: extendsextendsextendsContoh 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

dan

: extendsinterfaceclass gabungan jenis

    "Warisan kelas" dalam konsep berorientasikan objek
  • Seterusnya, mari lihat
yang digunakan untuk menyatakan kekangan jenis generik

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 的运行结果:

Mari berbincang dengan anda tentang kata kunci lanjutan dalam typeScript

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

  • Dalam kes 1, any ialah set induk number Mengapakah ia boleh diberikan kepada nilai jenis number?
  • Dalam kes 1, number | string hendaklah set induk number, jadi ia tidak boleh diberikan kepada nilai jenis number.
  • Dalam kes 1, number & string sepatutnya menjadi set induk number Secara logiknya, ralat harus dilaporkan di sini, tetapi mengapa tidak?
  • Dalam kes 2, {a:1} ialah subset daripada {a:1,b:2} Secara logiknya, ia boleh ditetapkan kepada nilai jenis {a:1,b:2} Mengapa ralat dilaporkan?
  • Dalam kes 3, saya rasa {name: '鲨叔'} ialah subset daripada SubClass Secara logiknya, ia boleh ditetapkan kepada nilai jenis SubClass Mengapa ralat dilaporkan?
  • Dalam kes 3, saya rasa 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:

  • Dalam kes 1, 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
  • tidak boleh difahami menggunakan model mental 子集类型 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:

  • harus difahami menggunakan konsep "jenis induk" dan "subjenis" peraturan diikuti di sebalik kekangan jenis yang memuaskan;
  • Dalam kekangan jenis AType extends BType, jika AType ialah subjenis BType, maka kami akan mengatakan bahawa AType memenuhi jenis yang kami isytiharkan Terkandas; >
  • Nilai dua jenis perhubungan jenis ibu bapa-anak mengikut "rajah hierarki jenis ts" berikut:
Nota: 1)

Ia bermaksud "A ialah jenis induk B, dan B ialah subjenis A"; 2) Selepas bendera kompilasi strictNullChecks dihidupkan, A -> B, undefined dan void tidak akan menjadi lapisan sistem jenis TypeScript kerana ia tidak boleh ditugaskan kepada jenis lain. null

Mari berbincang dengan anda tentang kata kunci lanjutan dalam typeScript

Mengenai gambar di atas, terdapat beberapa perkara yang boleh diserlahkan secara berasingan:

  • Ia ada di mana-mana. Ia adalah subjenis apa-apa jenis dan superjenis apa-apa jenis, malah mungkin apa-apa jenis itu sendiri. Oleh itu, ia boleh diberikan kepada mana-mana jenis; apabila any
  • bertindak sebagai jenis typeScript, ia mempunyai makna istimewa - ia sepadan dengan kedudukan {} dalam rantai prototaip js, dan ia adalah dianggap sebagai semua Kelas asas jenis objek. Subjenis bagi bentuk literal (Object.prototype.__proto__)=null
  • ialah array dan subjenis bentuk literal tuple ialah function. Kedua-dua 函数表达式类型 dan tuple disertakan dalam 函数表达式类型. 字面量类型
Sekarang mari kita gunakan model mental baharu ini untuk memahami

Contoh 2-1 Di mana ralat dilaporkan:

  • 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高级教程

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!

Kenyataan:
Artikel ini dikembalikan pada:juejin.cn. Jika ada pelanggaran, sila hubungi admin@php.cn Padam