TypeScript细碎知识点:如何理解 ts 中的 extends 和 infer

一、extends

extends 关键字在 TypeScript 中有多种应用,包括泛型约束、继承类、接口继承和条件类型。通过灵活使用 extends,TypeScript 提供了丰富的工具来增强类型安全性,使代码更具表现力和可维护性。

多种用法

🐹 1. 约束接口的继承

extends 关键字也可用于接口。通过接口继承,我们可以创建一个继承另一个接口的新接口,并添加额外的属性或方法。

//父类型
interface Person {
    name: string
    age: number
}

//子类行 继承 父类型
interface Employee extends Person {
    employeeId: number;
}

//实现
const employee: Employee = {
    name: 'John',
    age: 30,
    employeeId: 12345
}
🐹 2. 约束类的继承

在 TypeScript 中,extends 关键字也用于类的继承。子类可以继承父类的属性和方法,并在需要时进行重写。

class Animal {
    //父类属性
    name: string
    //父类构造函数
    constructor(name: string) {
        this.name = name
    }
}

class Dog extends Animal {
    //子类属性
    breed: string
    //子类构造函数
    constructor(name: string, breed: string) {
        super(name)
        this.breed = breed
    }
}

//实现
const myDog = new Dog("Fido", "Golden Retriever");
console.log(myDog.name); // 访问父类属性
console.log(myDog.breed); // 访问子类属性
🐹 3. 约束泛型类型参数

在 TypeScript 中,泛型(generics)使我们能够编写可重用的函数、类和组件,同时保持类型的安全性。extends 关键字在泛型中常常用于约束泛型类型参数,以确保传入的类型符合某些要求。

function lengthOfArray<T>(arr: T[]): number {
    return arr.length
}

const numbers = [1, 2, 3]
const result = lengthOfArray(numbers)

在上面的示例中,T 是泛型类型参数,它可以是任何类型。但有时我们希望泛型参数必须是某种类型的子类型。这时可以使用 extends 关键字来添加约束:

function firstElement<T extends Array<any>>(arr: T): T[0] {
    return arr[0];
}

const numbers = [1, 2, 3];
const firstNum = firstElement(numbers); // firstNum 的类型是 number

<T extends Array> 表示 T 必须是 Array 或其子类型。这确保了传入的参数 arr 是一个数组,从而允许我们安全地访问其第一个元素。

🐹 4. 条件类型

在 TypeScript 2.8+ 中,extends 关键字还被用于条件类型。条件类型使我们能够基于类型参数的属性来确定最终的类型。

type NonNullable<T> = T extends null | undefined ? never : T;

const x: string | null = "hello";
const y: string = x; // 编译通过

const a: string | null = null;
const b: string = a; // 报错

在上述示例中,NonNullable 是一个条件类型,它检查泛型类型 T 是否是 null 或 undefined,如果是,则返回 never 类型,否则返回 T 类型。这允许我们确保某个值不会为 null 或 undefined。

⚠️注意:extends前面是一个范型,并且当传入该参数的是联合类型时:
type A1 = 'x' extends 'x' ? string : number; // string 
type A2 = 'x' | 'y' extends 'x' ? string : number; // number

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

P是带参数T的泛型类型,其表达式和A1,A2的形式完全相同,A3是泛型类型P传入参数’x’ | 'y’得到的类型,A3和A2的类型相似,但是结果不同,出现这个结果的原因是所谓的分配条件类型

对于使用extends关键字的条件类型(即上面的三元表达式类型),如果extends前面的参数是一个泛型类型,当传入该参数的是联合类型,则使用分配律计算最终的结果。分配律是指,将联合类型的联合项拆成单项,分别代入条件类型,然后将每个单项代入得到的结果再联合起来,得到最终的判断结果。

上面的例子可以拆分

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

/* 【拆分流程】
 * P<'x' | 'y'> => P<'x'> | P<'y'>
 * P<'x'> = 'x' extends 'x' ? string : number => string
 * P<'y'> = 'y' extends 'x' ? string : number => number
 * P<'x' | 'y'> = P<'x'> | P<'y'> = string | number
 */

总之,满足两个要点即可适用分配律:

  • 参数是泛型类型,
  • 代入参数的是联合类型

在泛型的条件判断中还有些特殊的情况

  • 特殊的never

    // never是所有类型的子类型
    type A1 = never extends 'x' ? string : number; // string
    
    type P<T> = T extends 'x' ? string : number;
    type A2 = P<never> // never

    never被认为是空的联合类型,也就是说,没有联合项的联合类型,所以还是满足上面的分配律,然而因为没有联合项可以分配,所以P的表达式其实根本就没有执行,所以A2的定义也就类似于永远没有返回的函数一样,是never类型的。

  • 防止条件判断中的分配

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

    在条件判断类型的定义中,将泛型参数使用[]括起来,即可阻断条件判断类型的分配,此时,传入参数T的类型将被当做一个整体,不再分配 

二、infer

条件类型的基本语法是:

 T extends U ? X : Y;

如果占位符类型U是一个可以被分解成几个部分的类型,譬如数组类型元组类型函数类型字符串字面量类型等。这时候可以通过infer来获取U类型中某个部分的类型。

官方解释:现在在有条件类型的 extends 子语句中,允许出现 infer 声明,它会引入一个待推断的类型变量。 这个推断的类型变量可以在有条件类型的 true 分支中被引用。 允许出现多个同类型变量的 infer。
通俗的说:在有条件类型的 extends 子语句中,想要获取哪块的变量类型就在哪里用infer标注这个类型,

!!!重点要记住:

  • 只能出现在有条件类型extends的子语句中
  • 引入infer会出现一个待推断的变量
  • 推断的变量只能在truef分支中被引用 。

结合实际范例理解一下?

🐹 1. 获取函数的参数类型(Parameters)

比如我们这里想把函数fn的参数类型取出来,该怎么办?

function fn1(width: number, height: number): number {
    return width + height;
}

我们可以

function fn1(width: number, height: number): number {
    return width + height
}
//定义类型
type getFuncParamsType<T> = T extends (...args: infer p) => any ? p : T
type a = getFuncParamsType<typeof fn1>

整个过程呢,你可以简单的理解为getFuncParamsType接受一个泛型,如果T类型能被赋值给(...args:infer p) => any 则返回P,否则返回T

👀 悄悄告诉你,其实ts中也内置了这个类型,名字叫Parameters<T>,平时项目中你可以直接用。

由此,我们可以知道,关于infer

  • 只能出现在有条件类型extends的子语句中
  • 引入infer会出现一个待推断的变量
  • 推断的变量只能在truef分支中被引用 
💡通俗的来说:想要获取哪块的变量类型就在哪里用infer标注这个类型

我们经过上面的分析,下面的如何获取函数返回值类型,是不是已经呼之欲出了。

🐹 2. 获取函数的返回值类型(ReturnType)
function fn2(): boolean {
    return true;
}

我们可以

function fn2(): boolean {
    return true;
}
//定义类型
type getFuncReturnType<T> = T extends (...args: any) => infer p ? p : T
type a = getFuncReturnType<typeof fn2>

整个过程你可以这样理解,我们声明了一个类型getFuncReturnType<T>类型,然后我们想获取函数的返回值的类型,我们就将infer P标成函数的返回值,如果为T类型可以赋值给() => infer P直接返回P,否则直接返回T

👀 悄悄告诉你,其实该方法,ts已内置,名字叫ReturnType<T>
🐹 3. 获取构造函数参数类型(ConstructorParameters)

我们想获取构造函数的参数类型,是不是只要把infer P标注在构造函数参数的位置就可以

class People {
    name: string;
    constructor(name: string) {
        this.name = name
    }
}

//定义类型
type GetContructor<T> = T extends new (...args: infer P) => any ? P : T;
type a = GetContructor<typeof People>

👀 悄悄告诉你,其实该方法,ts已内置,名字叫ConstructorParameters<T>
🐹 4. 获取实例类型(InstanceType)

我们想获取构造函数People的实例类型

class People {
    name: string;
    constructor(name: string) {
        this.name = name
    }
}
//定义类型
type GetInstanceType<T> = T extends new (...args: any) => infer P ? P : never;
type a = GetInstanceType<typeof People>

👀 悄悄告诉你,其实该方法,ts已内置,名字叫InstanceType<T>
🐹 5. 获取this参数的类型(ThisParameterType)

我们想获取fn1函数的this类型。

function fn1(this: { name: string; age: number }) {
    this.name = "杨志强";
    this.age = 23;
}
// 定义类型
type GetFuncThisType<T> = T extends (this: infer P, ...args: any) => any ? P : T;
type a = GetFuncThisType<typeof fn1>;

👀 悄悄告诉你,其实该方法,ts已内置,名字叫ThisParameterType<T>
🐹 6. 剔除this参数(OmitThisParameter)
type OmitThisParameter<T> = unknown extends ThisParameterType<T> 
? T 
: T extends (...args: infer A) => infer R 
? (...args: A) => R 
: T;

以下语法是判断是否包含this参数。

unknown extends ThisParameterType<T> 

实战中积累的infer用法

🐹 1. 判断俩个类型是否相同?
type isEqual<T, U> = ((p1: T, p2: U) => void) extends ((...args: infer P) => void) ? P[number] extends T ? true : false : false;
type a = ["a"];
type b = [1];
type res = isEqual<a, b>;

我们可以这样理解:类型isEqual接受俩个泛型,利用infer的特性得到参数类型是一个数组,再取出下标为number的每一项,组成一个联合类型,接着看获取到的联合类型能否赋值给TU也可以),从而就可以判断是否为相同类型。

posted on 2024-04-10 16:12  梁飞宇  阅读(380)  评论(0)    收藏  举报