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的每一项,组成一个联合类型,接着看获取到的联合类型能否赋值给T(U也可以),从而就可以判断是否为相同类型。
浙公网安备 33010602011771号