好得很程序员自学网

<tfoot draggable='sEl'></tfoot>

TypeScript中extends的正确打开方式详解

前言

最近完整地看了一遍 TypeScript 的官方文档,发现文档中有一些知识点没有专门讲解到,或者是讲解了但却十分难以理解,因此就有了这一系列的文章,我将对没有讲解到的或者是我认为难以理解的知识点进行补充讲解,希望能给您带来一点帮助。

tips:配合官方文档食用更佳

这是本系列的第二篇TypeScript中extends的正确打开方式,在 TypeScript 中我们经常见到 extends 这一关键字,我们可能第一时间想到的就是继承,但除了继承它其实还有其他的用法,接下来让我们来一一获得 extends 的正确打开方式。

extends第一式:继承

作为众人皆知的 extends 关键字,其最出名的使用方式便是继承。

类继承类

首先让我们来用 extends 实现一下类的继承。

class Animal {
  public name;
  constructor(name: string) {
    this.name = name;
  }
  eat(food: string) {
    console.log(`${this.name}正在吃${food}`);
  }
}
class Sheep extends Animal {
  constructor(name: string) {
    super(name);
  }
  miemie() {
    console.log("别看我只是一只羊,羊儿的聪明难以想象~");
  }
}
let lanyangyang = new Sheep("懒羊羊");
lanyangyang.eat("青草蛋糕");
// 懒羊羊正在吃青草蛋糕
lanyangyang.miemie();
//别看我只是一只羊,羊儿的聪明难以想象~

首先我们定义了一个 Animal 类,该类有 name 属性以及 eat 方法。然后又定义了一个继承 Animal 类的 Sheep 类,该类在父类 name 属性以及 eat 方法基础上又新增了一个 miemie 方法。

接口继承接口

extends 不仅能够用于类与类之间的继承上,还能够用于接口与接口之间的继承。接下来我们来实现一下接口之间的继承。

interface IAnimal{
  name:string;
  eat:(food:string)=>void;
}
interface ISheep extends IAnimal{
  miemie:()=>void;
}
let lanyangyang:ISheep={
  name:'懒羊羊',
  eat(food:string){
    console.log(`${this.name}正在吃${food}`);
  },
  miemie() {
    console.log("别看我只是一只羊,羊儿的聪明难以想象~");
  }   
}
lanyangyang.eat("青草蛋糕");
// 懒羊羊正在吃青草蛋糕
lanyangyang.miemie();
//别看我只是一只羊,羊儿的聪明难以想象~

我们定义了一个 IAnimal 接口,然后用通过 extends 继承 IAnimal 定义了 ISheep 接口,则实现 ISheep 接口的变量 lanyangyang 必须要有父接口的 name 属性以及实现 eat 方法,并且还要实现本身的 miemie 方法。

现在我们通过 extends 实现了类与类之间的继承、接口与接口之间的继承,那么类与接口之间是否能互相继承呢?答案是可以。

接口继承类

首先我们使用 extends 来实现接口继承类。

class Animal {
  public name;
  constructor(name: string) {
    this.name = name;
  }
  eat(food: string) {
    console.log(`${this.name}正在吃${food}`);
  }
  static run(){
    console.log(`${this.name} is running`)
  }
}
interface ISheep extends Animal{
  miemie:()=>void;
}
let lanyangyang:ISheep={
  name:'懒羊羊',
  eat(food:string){
    console.log(`${this.name}正在吃${food}`);
  },
  miemie() {
    console.log("别看我只是一只羊,羊儿的聪明难以想象~");
  }   
}
lanyangyang.eat("青草蛋糕");
// 懒羊羊正在吃青草蛋糕
lanyangyang.miemie();
//别看我只是一只羊,羊儿的聪明难以想象~

在接口继承类时,可以把类看作一个接口,但是类中的静态方法是不会继承过来的。我们在实现 ISheep 接口的变量 lanyangyang 必须要有类 Animal 的 name 属性以及实现 eat 方法,并且还要实现本身的 miemie 方法。但是我们不必实现类 Animal 的静态方法 run 。

是不是觉得还会有下一个标题 类继承接口 ,对不起,这个真没有!类继承接口使用的关键字变成了 implements 。

extends第二式:三元表达式条件判断

extends 还有一个比较常见的用法就是在三元表达式中进行条件判断,即判断一个类型是否可以分配给另一个类型。这里根据三元表达式中是否存在泛型判断结果还不一致,首先我们介绍普通的三元表达式条件判断。

普通的三元表达式条件判断

带有 extends 的三元表达式如下:

type TypeRes=Type1 extends Type2? Type3: Type4;

这里表达的意思就是如果类型 Type1 可被分配给类型 Type2 ,则类型 TypeRes 取 Type3 ,否则取 Type4 。那怎么理解类型 Type1 可被分配给类型 Type2 呢??

我们可以这样理解:类型为 Type1 的值可被赋值给类型为 Type2 的变量。可以具体分为一下几种情况:

Type1 和 Type2 为同一种类型。 Type1 是 Type2 的子类型。 Type2 类型兼容类型 Type1 。 接下来我们分情况进行验证。

情况一: Type1 和 Type2 为同一种类型。

type Type1=string;
type Type2=Type1;
type Type3=true;
type Type4=false;
type TypeRes=Type1 extends Type2? Type3: Type4;
// true

这里 Type1 和 Type2 为同一种类型。因此 Type1 可被分配给 Type2 。因此 TypeRes 类型最后取为 true 。

情况二: Type1 是 Type2 的子类型。

class Animal {
  public name;
  constructor(name: string) {
    this.name = name;
  }
  eat(food: string) {
    console.log(`${this.name}正在吃${food}`);
  }
}
class Sheep extends Animal {
  constructor(name: string) {
    super(name);
  }
  miemie() {
    console.log("别看我只是一只羊,羊儿的聪明难以想象~");
  }
}
type Type1=Sheep;
type Type2=Animal;
type Type3=true;
type Type4=false;
type TypeRes=Type1 extends Type2? Type3: Type4;
// true

这里 Sheep 类继承自 Animal ,即 Type1 是 Type2 的子类型。因此 Type1 可被分配给 Type2 。因此 TypeRes 类型最后取为 true 。

情况三: Type2 类型兼容类型 Type1 。

首先还是抛出一个问题,什么是 类型兼容 ??这个问题可以从官方文档中得到答案,大家可以戳 类型兼容性 详细了解!

所谓 Type2 类型兼容类型 Type1 ,指得就是 Type1 类型的值可被赋值给类型为 Type2 的变量。 举个栗子:

type Type1={
  name:string;
  age:number;
  gender:string;
}
type Type2={
  name:string;
  age:number;
}
type Type3=true;
type Type4=false;
type TypeRes=Type1 extends Type2? Type3: Type4;
// true

由于类型 Type1 拥有至少与 Type2 相同的属性,因此 Type2 是兼容 Type1 的。也就是说 Type1 类型的值可被赋值给类型为 Type2 的变量。

let kenny1:Type1={
  name:'kenny',
  age:26,
  gender:'male'
}
let kenny2:Type2=kenny;
// no Error

因此 TypeRes 类型最后取为 true 。

再举个栗子,以函数的兼容性为例,

type Type1=(a:number)=>void;
type Type2=(a:number,b:string)=>void;
type Type3=true;
type Type4=false;
type TypeRes=Type1 extends Type2? Type3: Type4;
// true

当函数参数不同时,看 Type2 是否兼容 Type1 ,就要看 Type1 的每个参数必须能在 Type2 里找到对应类型的参数。 注意的是参数的名字相同与否无所谓,只看它们的类型。

这里 Type1 第一个 number 类型的参数是可以在 Type2 中找到,即 Type1 类型的函数可被赋值给类型为 Type2 的变量。

let fn1:Type1=(a:number)=&gt;{}
let kenny2:Type2=fn1;
// no Error

因此 TypeRes 类型最后取为 true 。

带有泛型的三元表达式条件判断

我们先来一个举一个不带泛型的栗子,大家可以想想结果是什么。

type Type1=string|number;
type Type2=string;
type Type3=true;
type Type4=false;
type TypeRes=Type1 extends Type2? Type3: Type4;

没错,由于 Type1 是 Type2 的父类型,因此 Type1 是不可分配给 Type2 的,因此 TypeRes 类型最后取为 false 。

但当我们加上泛型之后呢,再来看一个栗子。

type Type1=string|number;
type Type2=string;
type Type3=true;
type Type4=false;
type Type5<T>=T extends Type2? Type3: Type4;
type TypeRes<Type1>
// boolean

这里 TypeRes 类型最后就不是 false 了,而变成 boolean 。这是为什么呢?

原来再使用泛型时,若extends左侧的泛型具体取为一个联合类型时,就会把联合类型中的类型拆开,分别带入到条件判断式中进行判断,最后把结果再进行联合。上述的栗子中结果可以这么来看,

(string extends string?true:false)|(number extends string?true:false)
true | false
boolean

在高级类型中有很多类型实现便用到了这一特性。

比如 Exclude<T, U>  -- 从 T 中剔除可以赋值给 U 的类型。

type T1 = "a" | "b" | "c" | "d";
type T2 = "a" | "c" | "f"
type ExcludeT1T2=Exclude<T1,T2> //"b"|"d"

该类型的类型实现为

type Exclude<T, U> = T extends U ? never : T;

当 T 为联合类型时,会自动分发条件,对 T 中的所有类型进行遍历,判断其是否可以分配给类型 U ,如果是的话便返回 never 类型,否则返回其原来的类型。最后再将其进行联合得到一个结果联合类型。

由于 never 类型与其他类型联合最终得到的还是其他类型,因此便可以从类型T中剔除掉可以赋给U的类型。

那有没有办法让泛型三元表达式中 extends 和普通的 extends 作用相同?有!只需要给泛型加一个 [] 。栗子如下:

type Type1=string|number;
type Type2=string;
type Type3=true;
type Type4=false;
type Type5<T>=[T] extends Type2? Type3: Type4;
type TypeRes<Type1>
// false

extends第三式:泛型约束

首先我们来回答一下什么是泛型?简单来说,泛型就是一种类型变量,普通的变量代表一个任意的值,而不是一个特定的值,我们可以把任何值赋给变量,而类型变量代表一个任意的类型,而不是一个特定的类型,我们可以把任何类型赋给类型变量。它是一种特殊的变量,只用于表示类型而不是值。

那如果我们不想让泛型表示任意类型时,该怎么办?这时我们就可以使用 extends 对泛型进行约束,让泛型表示满足一定条件的类型。接下来,我们使用 extends 进行泛型的约束。

interface ISheep{
  name:string;
  eat:(food:string)=>void;
  miemie:()=>void;
}
function eatAndMiemie<T extends ISheep>(sheep:T):void{
    sheep.eat("青草蛋糕");
    sheep.miemie();
}
eatAndMiemie(
{
  name: "懒羊羊",
  eat(food:string){
    console.log(`${this.name}正在吃${food}`);
  },
  miemie() {
    console.log("别看我只是一只羊,羊儿的聪明难以想象~");
  }   
  run() {console.log(`${this.name}正在奔跑`)};
  }
)
// 懒羊羊正在吃青草蛋糕
//别看我只是一只羊,羊儿的聪明难以想象~

这里我们便对泛型 T 进行了约束,其必须至少要拥有 ISheep 的 name 属性及 eat 、 miemie 方法,另外 T 中若有其他的属性及方法,则不作限制。这里我们便通过 extends 对泛型 T 进行了约束。

其实泛型约束中的 extends 也是起到了三元表达式中类型分配的作用,其中 T extends ISheep 表示泛型 T 必须可以分配给类型 Isheep 。

以上就是TypeScript中extends的正确打开方式详解的详细内容,更多关于TypeScript extends打开方式的资料请关注其它相关文章!

查看更多关于TypeScript中extends的正确打开方式详解的详细内容...

  阅读:45次