【TypeScript】extends关键字的三种用法及类型操作实战

6,320 阅读8分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第2天,点击查看活动详情

使用

在各种类型操作中,少不了extends关键字的身影,它主要有以下几个作用: 接口继承 类型约束以及条件类型

接口继承

    interface Person {
        name: string;
        age: number;
    }
    
    interface Player extends Person {
        item: 'ball' | 'swing';
    }
    //接口继承后
    // interface Person {
    //     name: string;
    //     age: number;
    //     item: 'ball' | 'swing';
    // }

类型约束

通常和泛型一起使用,那么具体应该如何使用呢?

    interface Dog {
      bark: () => void
    }
    ​
    function dogBark<T extends Dog>(arg: T) {
      arg.bark()
    }

我们定义类型Dog,它 有一个不返回任何值的bark方法,使用extends关键字进行泛型约束,传入dogBark方法的值必须有bark方法,简单的说extends关键字在这里的作用:作为一个守门员,只让会狗叫的进,管你是不是🐕,只要会狗叫,就可以进;如果不会,请出门右转。

image.png

   let dogA = {
     weight: 12,
     age: 4
   }
   ​
   let dogB  ={
     weight: 12,
     age: 4,
     bark: () => console.log('dogB is barking')
   }
   ​
   dogBark(dogA)       
   // error !!!
   // Argument of type '{ weight: number; age: number; }' is not assignable to parameter of type 'Dog'.
   // Property 'bark' is missing in type '{ weight: number; age: number; }' but required in type 'Dog'.dogBark(dogB)      // success: "dogB is barking" 

在使用extends关键字实现一些类型时,可能会用到如下代码:

  P extends keyof T

表示P的类型是keyof T返回的字面量联合类型,也就是说P原本没限制,是any,限制之后类型变成了keyof T返回的字面量联合类型。

类似的有使用T extends keyof any对对象类型的属性进行约束,keyof any返回的是string | number | symbol,即这也是属性字段的取值范围。

再来看这样一个例子

interface Person {
    name: string;
    age: number;
}

type NameOf<T> = T['name'] // error:  Type '"name"' cannot be used to index type 'T'

NameOf类型的作用是取得传入类型Tname属性的值的类型,但这里却报错了。因为传入的泛型T不一定有属性name, 传入的可能是一个没有name属性的对象,也可能是一个字面量类型,访问T可能没有的属性是不安全的,因此会报错,要解决这个问题就需要对泛型T进行约束,确保其一定具有name这个属性。

interface Person {
    name: string;
    age: number;
}

type NameOf<T extends {'name': unknown}> = T['name'] // success!
type personName = NameOf<Person>  //string

条件类型 (Conditional Types )

常见表现形式为:

 T extends U ? 'Y' : 'N'

可以这样理解:TU的子类型,那么返回结果是'Y', 否则是'N'. 类似JS中的三元表达式,其工作原理是类似的,例如:

type res1 = true extends boolean ? true : false                  // true
type res2 = 'name' extends 'name'|'age' ? true : false           // true
type res3 = [1, 2, 3] extends { length: number; } ? true : false // true
type res4 = [1, 2, 3] extends Array<number> ? true : false       // true

要注意:

  • extends在条件类型中的作用和类型约束中的作用不一样
  • 条件类型只支持在type中使用

此外,extends作为条件类型时也是可以嵌套的,就像if语句一样。

type TypeName<T> =
 T extends string    ? "string" :
 T extends number    ? "number" :
 T extends boolean   ? "boolean" :
 T extends undefined ? "undefined" :
 T extends Function  ? "function" :
 "object";

 type T0 = TypeName<string>;  // "string"
 type T1 = TypeName<"a">;     // "string"
 type T2 = TypeName<true>;    // "boolean"
 type T3 = TypeName<() => void>;  // "function"
 type T4 = TypeName<string[]>;    // "object"


再来看如下代码:

    type A1 = P<'x' | 'y'> extends 'x' ? string : number; //  type A1 = number
    type P<T> = T extends 'x' ? string : number;
    type A2 = P<'x' | 'y'> // ?   type A2 = string | number 

A2结果为什么不是number呢?实际发生的操作类似如下:

    type A2 = P<'x' | 'y'> 
    type A2 = P<'x'>  | P<'y'>
    type A2 = ('x' extends 'x' ? string : number) | ('y' extends 'x' ? string : number)
    type A2 = string | number

这叫分配条件类型(Distributive Conditional Types

T为泛型时,且传入该泛型的是一个联合类型,那么该联合类型中的每一个类型都要进行上述操作,最终返回上述操作结果组成的新联合类型。换句话说,这里的分配是指将上述提到的"三元表达式"操作应用于联合类型中的每个成员。

要注意的是:

1. extends关键字左侧的是一个泛型,且传入泛型的必须是联合类型,其他类型如交叉类型是没有分配效果的。

如果左侧不是泛型,直接传入一个联合类型,是没有分配效果的,只是一个简单的条件判断。

  type A1 = 'x' extends 'x' ? string : number; // string

  type A2 = 'x' | 'y' extends 'x' ? string : number; // number
// 如果分配生效的话,结果应该是 string | number

2. 分配操作只有在检查的类型是naked type parameter时才生效。

那么是什么是naked type parameter呢?直接翻译过来怪怪的,参数是的? 我的理解是没有对传进来的泛型参数进行一些额外操作,那么就符合naked type parameter的要求。

看一下以下的例子,更容易理解。这也是stackoverflow上一个高赞回答的例子

type NakedUsage<T> = T extends boolean ? "YES" : "NO"
type WrappedUsage<T> = [T] extends [boolean] ? "YES" : "NO"; // wrapped in a tuple,type Distributed = NakedUsage<number | boolean > // = NakedUsage<number> | NakedUsage<boolean> =  "NO" | "YES" 
type NotDistributed = WrappedUsage<number | boolean > // "NO"    
type NotDistributed2 = WrappedUsage<boolean > // "YES"

其中,WrappedUsage对传入的泛型参数进行了操作,不属于naked type parameter,因此不会进行分配操作。

类型操作实战

Pick & Record

extends类型约束特性相关的工具类型有PickRecord

Pick

Pick表示从一个类型中选取指定的几个字段组合成一个新的类型,用法如下:

type Person = {
  name: string;
  age: number;
  address: string;
  sex: number;
}

type PickResult = Pick<Person, 'name' | 'address'>
// { name: string; address: string; }

实现方式

type Pick<T, K extends keyof T> = {
    [P in K]: T[P];
};

首先进行了类型限定,K一定是T的子集,然后用in遍历K中的每个属性, T[P]是属性对应的值。

Record

Record<K, T>用来将K的每一个键(k)指定为T类型,这样由多个k/T组合成了一个新的类型,用法如下:

type keys = 'Cat'|'Dot'
type Animal = {
  name: string;
  age: number;
}

type RecordResult = Record<keys, Animal>
// result: 
// type RecordResult = {
//     Cat: Animal;
//     Dot: Animal;
// }

实现方式

type Record<K extends keyof any, T> = {
    [P in K]: T
}

keyof any是什么鬼?鼠标放上去看看就知道了

因此,keyof anystring | number | symbol,先对键的取值范围进行了限定,只能是这三者中的一个。

Exclude & Extract & NonNullable

extends条件类型特性相关的工具类型又有哪些呢?

先看着两个:ExcludeExtract

Exclude<T, U>: 排除T中属于U的部分

image.png Extract<T, U>: 提取T中属于U的部分,即二者交集

image.png

使用方法

type ExcludeResult = Exclude<'name'|'age'|'sex', 'sex'|'address'>
//type ExcludeResult = "name" | "age"
type ExcludeResult = Extract<'name'|'age'|'sex', 'sex'|'address'>
//type ExcludeResult = "sex"

实现方式

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

实现思路不再赘述,见前文extends分配条件类型的原理

NonNullable工具类型可以从目标类型中排除nullundefined,和Exclude相比,它将U限定的更具体。

实现也很简单:

type A = null | undefined | 'dog' | Function// type nonNullable<T> = Exclude<T , undefined | null>
type nonNullable<T> = T extends null | undefined ? never : T
​
type res = nonNullable<A>   // type res = Function | "dog"

Omit

根据已经实现的Exclude类型,可以实现Omit类型,Omit<T, K>:删除T中指定的字段,用法如下:

type Person = {
  name?: string;
  age: number;
  address: string;
}

type OmitResult = Omit<Person, 'address'>
// 结果:{ name?: string; age: number; }

实现方式

type Omit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>

首先,删除指定字段,字段类型限定在 string | symbol number中,然后用ExcludeT的属性所组成的字面量联合类型中移除指定字段,形成新的联合类型;最后利用Pick选取指定字段生成新的类型

AppendToObject

AppendToObject的作用是向指定对象中添加一个属性, 同时指定属性值的类型。如果该属性字段之前就存在,新增的字段会被忽略。

注:该类型并不是内置工具类型

使用方式

type Test = { id: '1' }
// expected to be { id: '1', value: 4 }
type Result = AppendToObject<Test, 'value', 4> 
// 结果:{ id: number; name: string; }
type result = AppendToObject<{ id: number; }, 'name', string>

实现方式

type AppendToObject<T, K extends keyof any, V> = {
  [P in keyof T | K]: P extends keyof T ? T[P] : V
}

首先, 需要遍历的所有属性包含T中的属性字段和新增的字段K,即keyof T | K,然后使用in关键字进行遍历操作,对遍历到的每个属性字段使用extends进行判断,如果遍历到的字段P是原本T中就存在的属性字段,判断为true,返回T[p];否则为false,说明该属性字段之前并不存在,返回新增字段对应的类型V

Merge

Merge将两个类型合并成一个类型,第二个类型的键会覆盖第一个类型的键。

使用方式

type foo = {
  name: string;
  age: string;
}

type coo = {
  age: number;
  sex: string
}

type Result = Merge<foo,coo>; // expected to be {name: string, age: number, sex: string}

实现方式

type Merge<F, S> = {
  [P in  keyof F | keyof S]: P extends keyof S ? S[P] : P extends keyof F ? F[P] : never
}

这里使用了两次extends, 写成下方这种形式可能更清楚一些:

type Merge<F, S> = {
  [P in  keyof F | keyof S]: 
    P extends keyof S 
      ? S[P] 
      : (P extends keyof F ? F[P] : never)
}

AppendToObject一样,首先用in关键字遍历所有的属性字段(keyof F | keyof S), 在此过程中对每一字段进行相应判断,因为S中的对应的字段会对F中相同字段进行覆盖,因此先判断该字段是否属于S,然后再判断该字段是否属于P

参考

ts handbook

TypeScript 的 extends 条件类型

type challenges