彻底理解TypeScript泛型语法

泛型Generics)是 TypeScript 中用于创建可复用组件的功能

  • 可以在类、函数、接口等定义中使用,使它们能够处理不同类型的数据,而不局限于某一种特定的数据类型

  • 泛型可以在不丢失类型检查的前提下,编写可扩展和复用性高的代码

认识

一个需求:封装一个函数,传入一个参数,并且返回这个参数

  • 如果是TypeScript的思维方式,要考虑这个参数和返回值的类型需要一致

  • 虽然any是可以的,但是定义为any的时候,其实已经丢失了类型信息

    ts 复制代码
    function fn(arg: any) :any {
      return arg
    }
    fn(123) // function fn(arg: any): any,不能根据传入的确定类型
  • 需要在这里使用一种特性的变量 - 类型变量(type variable),它作用于类型而不是值

  • 这就有了泛型,在开发中用的也比较多

使用

  • 定义时使用<Type>可以多个,使用时通过 <类型> 的方式将类型传递给函数、接口等

  • Type 是一个类型变量,表示一个占位符,调用时由传入参数的类型决定

  • 常见的占位符名称:

    • TType的缩写,类型

    • K、Vkeyvalue的缩写,键值对

    • EElement的缩写,元素

    • OObject的缩写,对象

  • 理论比较模糊下面看代码具体学习泛型的应用

泛型函数

使用泛型定义一个函数,它能够根据传入的参数类型自动推断类型,而不是固定为某一种类型

  • 基本使用:

    ts 复制代码
    function genericsFn1<T>(arg: T): T { // 说明这个函数的参数和返回值都是调用函数时传入的类型 <类型>
      return arg
    }
    
    // <string> 这里的string会传递给函数什么中的T,函数genericsFn就为 function genericsFn<string>(arg: string): string
    console.log(genericsFn1<string>('stringType')) 
  • 多个参数:

    ts 复制代码
    function genericsFn2<T, V>(msg: T, value: V): V {
      console.log(msg)
      return value
    }
    
    // 不传泛型会进行类型推断 function genericsFn2<string, 123>(msg: string, value: 123): 123
    console.log(genericsFn2('hello', 123)) 
    
    console.log(genericsFn2<string>('hello', 123)) // 因为写了两个,只传一个会报错,但写了默认类型就不会报错
    
    // function genericsFn2<string, number>(msg: string, value: number): number
    console.log(genericsFn2<string, number>('hello', 123)) 
  • 默认类型:

    ts 复制代码
    function genericsFn3<T, V = number>(msg: T, value: V): V {
      console.log(msg)
      return value
    }
    // 不传就使用默认类型:function genericsFn3<string, number>(msg: string, value: number): number
    console.log(genericsFn3<string>('hello', 123))

泛型类

类似于泛型函数,类也可以定义泛型,从而能够处理不同类型的数据

  • 基本使用:

    ts 复制代码
    class Shape1<T> {
      constructor(public x: T, public y: T){
        this.x = x
        this.y = y
      }
      getArea(value: T): void{
        console.log(value)
      }
    }
    const s1 = new Shape1('10', '23') // constructor Shape<string>(x: string, y: string): Shape<string>
    s1.getArea('90') // Shape1<string>.getArea(value: string): void
    const s2 = new Shape1<number>(10, 20)
    s2.getArea(30) // Shape1<number>.getArea(value: number): void
  • 多个类型:

    ts 复制代码
    class Shape2<T, U> {
      constructor(public x: T, public y: U){
        this.x = x
        this.y = y
      }
      getArea<V>(value: V): void{
        console.log(value, this.x, this.y)
      }
    }
    const s1 = new Shape2(10, 20)
    s1.getArea('30')
    const s2 = new Shape2<number, string>(10, '20')
    s2.getArea<number>(30)
  • 默认参数:

    ts 复制代码
    class Shape3<T, U = string> {
      constructor(public x: T, public y: U){
        this.x = x
        this.y = y
      }
      getArea<V = number>(value: V): void{
        console.log(value, this.x, this.y)
      }
    }
    const s1 = new Shape3<number>(10, '20')
    s1.getArea(30)
    
    class Circle<T> extends Shape3<number> {
      constructor(x: number, y: string, public r:T) {
        super(x, y)
        this.r = r
      }
    }
    const c = new Circle(10, '20', 30)

泛型接口

也可以用泛型来定义接口和别名,使接口和别名能够接受不同的类型参数

  • 基本使用:

    ts 复制代码
    type IFoo<T> = {
      initValue: T
      valueList: T[]
      hanleValue: (value: T) => void
    }
    interface IFoo1<T> {
      initValue: T
      valueList: T[]
      hanleValue: (value: T) => void
    }
    
    /* 
      泛型接口传入number就为:
        interface IFoo1<number> {
          initValue: number
          valueList: number[]
          hanleValue: (value: number) => void
        }
    */
    const foo1: IFoo1<number> = {
      initValue: 0,
      valueList: [1,2,3],
      hanleValue(value) { // 这里value不写类型也能推出是number
        console.log(value)
      }
    }
  • 多个类型:

    ts 复制代码
    interface IFoo2<T, V> {
      initValue: T
      valueList: V[]
      hanleValue: (value: T) => V
    }
    const foo2: IFoo2<number, string> = {
      initValue: 0,
      valueList: ['abc', 'cba'],
      hanleValue(value) {
        return value.toString()
      }
    }
  • 默认类型:

    ts 复制代码
    interface IFoo3<T = number, V = string> {
      initValue: T
      valueList: V[]
      hanleValue: (value: T) => V
    }
    const foo3: IFoo3 = {
      initValue: 0,
      valueList: ['abc', 'cba'],
      hanleValue(value) {
        return value.toString()
      }
    }

泛型约束

在某些情况下,可能希望限制泛型只能是某些特定类型,这时可以使用泛型约束(Generic Constraints ,通过 <T extends SomeType> 给泛型添加条件限制T 必须是 SomeType 或其子类型,约束确保了泛型具有特定的属性或行为

  • 约束属性和方法

    • 比如stringarray都是有length的,或者某些对象也是会有length属性的

    • 那么只要是拥有length的属性都可以作为我们的参数类型,那么应该如何操作呢?

    ts 复制代码
    // interface ILength {
    //   length: number
    // }
    function foo<T extends { length: number }>(msg: T): T{
      return msg
    }
    foo({name: 'obj', length: 1})
    foo<(string | number)[]>(['abc', 123])
    foo<string>('abc')
  • 约束特定类型

    ts 复制代码
    function compare<T extends number | string>(a: T, b: T): boolean {
      return a === b;
    }
    
    compare(10, 20);  // 可以
    compare("hello", "world");  // 可以
    // compare(true, false); // 错误:boolean 不属于 number | string
  • 约束数组或对象的索引

    ts 复制代码
    function getValue<T, K extends keyof T>(obj: T, key: K) {
      return obj[key];
    }
    
    const person = { name: "Alice", age: 25 };
    const name = getValue(person, "name");  // 返回 "Alice"
    // const invalid = getValue(person, "height");  // 错误: 类型 'height' 不存在于 'person'
  • 约束类

    ts 复制代码
    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;
      }
    }
    
    function printAnimal<T extends Animal>(animal: T): void {
      console.log(animal.name);
    }
    
    const dog = new Dog("Rex", "Golden Retriever");
    printAnimal(dog);  // 输出: Rex

映射类型

允许你基于现有的类型进行转换或构造新类型。通过映射类型你可以对类型的所有属性应用相同的变换规则,例如使属性变为可选、只读等

  • 大部分内置的工具和类型体操题目都是通过映射类型来实现的

  • 映射类型主要使用 inkeyof 关键字完成,获取对象类型所有键,然后循环遍历键名创建一个类型

  • 其中 keyof 用于获取某个对象类型的所有键并返回它们的联合类型

  • in 关键字主要用于循环遍历枚举一个类型的键

基本使用:

ts 复制代码
// keyof作用
type Person = { name: string; age: number; address: string; }; 
type PersonKeys = keyof Person; // 'name' | 'age' | 'address'


interface IPerson1 {
  name: string
  age: number
}
type MapType1<T> = {
  [K in keyof T]: T[K]
}
type newMap1 = MapType1<IPerson1> // type newMap1 = { name: string; age: number; }

添加修饰符:

ts 复制代码
interface IPerson1 {
  name: string
  age: number
}
type MapType2<T> = {
  [K in keyof T]?: T[K]
}
type newMap2 = MapType2<IPerson1> // type newMap2 = { name?: string | undefined; age?: number | undefined; }

type MapType3<T> = {
  readonly [K in keyof T]: T[K]
}
type newMap3 = MapType3<IPerson1> // type newMap3 = { readonly name: string; readonly age: number; }

type MapType4<T> = {
  readonly [K in keyof T]?: T[K]
}
type newMap4 = MapType4<IPerson1> // type newMap4 = { readonly name?: string | undefined; readonly age?: number | undefined; }

减少修饰符: 可以通过前缀 - 或者 + 删除或者添加这些修饰符,如果没有写前缀,相当于使用了 + 前缀

ts 复制代码
interface IPerson2 {
  name?: string
  age?: number
}
type MapType5<T> = {
  [K in keyof T]-?: T[K]
}
type newMap5 = MapType5<IPerson1> // type newMap5 = { name: string; age: number; }

条件类型

很多时候,日常开发中需要基于输入的值来决定输出的值,同样也需要基于输入值的类型来决定输出值的类型 ,条件类型(Conditional types)就是用来帮助我们描述输入类型和输出类型之间的关系

语法T extends U ? X : Y

  • T:输入的类型

  • U:判断的目标类型

  • X :当 TU 的子类型时使用的类型

  • Y : 当 T 不是 U 的子类型时使用的类型

ts 复制代码
function sum<T extends number | string>(arg1: T, arg2: T): T extends string ? string : number 
function sum (arg1: any, arg2: any){
  return arg1 + arg2
}

console.log(sum<string>('aaa', 'bbb')) // aaabbb
console.log(sum<number>(10, 20)) // 30

推断

条件类型提供了 infer 关键词,可以从正在比较的类型中推断类型,然后在 true 分支里引用该推断结果

  • 语法T extends U ? infer V : X

    • T:输入的类型

    • U:判断的目标类型

    • infer V :在满足条件时推断出的类型变量 V

    • X:如果条件不成立时使用的类型

  • 案例

    • 假设我们有一个数组类型,希望从中提取出其元素的类型

      ts 复制代码
      type ElementType<T> = T extends (infer U)[] ? U : T;
      type A = ElementType<number[]>;  // number
      type B = ElementType<string[]>;  // string
      type C = ElementType<any[]> // any
      type D = ElementType<(number | string)[]> // string | number
      type E = ElementType<number>;  // number (非数组类型直接返回自己)
    • 假设我们有一个函数类型,希望从中提取出返回值类型,可以使用 infer

      ts 复制代码
      type ReturnType<T extends (...args: any[]) => any> = T extends (...args: any[]) => infer R ? R : never
      type A = ReturnType<() => string> // string
      type B = ReturnType<() => any[]> // any[]
      type C = ReturnType<() => number | string> // string | number
      type D = ReturnType<number> // never 不是函数类型报错

      为什么使用 never

      1. 当条件不满足时,想明确表示无效类型永远不会发生的情况,这时使用never最合适

      2. 传入的 T 不是一个函数类型时,无法提取出返回值类型。这时返回 never 表示:这里不会有有效的返回值类型,因为这个条件根本不成立

      为什么不用 undefined 或其他类型?

      1. 使用 never 更符合逻辑 。因为传入的类型不是函数类型,所以没有任何返回值类型可以被推断。never 表示这种不可能的情况

      2. never 的语义明确:它告诉我们,这种情况不应该发生,因此无法推断出任何合理的类型

分发

分发条件类型主要用于当类型参数是联合类型时,TypeScript 会对联合类型中的每个成员单独地应用条件类型进行计算,然后将结果重新组合成一个新的联合类型

  • 语法:T extends U ? X : Y

    • T 是一个联合类型时,T 的每个成员都会单独进行判断,并将结果重新组合成新的联合类型

    • T:输入的类型

    • U:判断的目标类型

    • X :当 TU 的子类型时使用的类型

    • Y : 当 T 不是 U 的子类型时使用的类型

  • 使用:

    ts 复制代码
    type DistributType<T> = T extends any ? T[] : never
    type A = DistributType<number | string> // number[] | string[]
    type B = DistributType<string> // string[]
    type C = DistributType<() => void> // (() => void)[]
  • 避免分发:

    有时希望避免分发行为,而是将整个联合类型作为一个整体来进行判断,可以通过包裹类型来阻止分发 ,例如使用元组:[T] 将整个联合类型包裹为一个整体,条件判断只会执行一次

    ts 复制代码
    type Example<T> = [T] extends [string] ? string : number;
    type Result = Example<'hello' | 42 | true>; // number

内置工具

Partial<Type>

用于构造一个Type下面的所有属性都设置为可选的类型

ts 复制代码
type OType = {
  name: string,
  age: number
}
type PartialType1 = Partial<OType> // type PartialType1 = { name?: string | undefined; age?: number | undefined; }

// 使用映射类型自己实现
type MyPartial<T> = {
  [P in keyof T]?: T[P]
} 
type PartialType2 = MyPartial<OType> // type PartialType1 = { name?: string | undefined; age?: number | undefined; }

Required<Type>

用于构造一个Type下面的所有属性全都设置为必填 的类型,这个工具类型跟 Partial 相反

ts 复制代码
type OType = {
  name: string,
  age?: number
}
type RequiredType1 = Required<OType> // type RequiredType1 = { name: string; age: number; }

type MyRequired<T> = {
  [P in keyof T]-?: T[P]
} 
type RequiredType2 = MyRequired<OType> // type RequiredType2 = { name: string; age: number; }

Readonly<Type>

用于构造一个Type下面的所有属性全都设置为只读的类型,意味着这个类型的所有的属性全都不可以重新赋值

ts 复制代码
type OType = {
  name: string,
  age?: number
}
type ReadonlyType1 = Readonly<OType> // type ReadonlyType1 = { readonly name: string; readonly age?: number | undefined;}

type MyReadonly<T> = {
 readonly [P in keyof T]: T[P]
}
type ReadonlyType2 = MyReadonly<OType> // type ReadonlyType2 = { readonly name: string; readonly age?: number | undefined;}

Record<Keys, Type>

用于构造一个对象类型,它所有的key(键)都是Keys类型,它所有的value(值)都是Type类型

ts 复制代码
type RecordType1 = Record<'上海' | 'aaa', number | string> // type RecordType1 = {aaa: string | number; 上海: string | number;}

type KeyOfAny = keyof any;  // 等价于:string | number | symbol
type MyRecord<K extends keyof any, T> = {
  [P in K]: T
} 
type RecordType2 = MyRecord<'上海' | 'aaa', number | string> // type RecordType2 = {aaa: string | number; 上海: string | number;}
  • keyof any 它表示所有可能的键的联合类型 ,获取到的是string | number | symbol,因为keyof作用是获取某个对象类型的所有键 , 后面写any时表示是任何类型对象、数组、函数等等,但无论它是什么类型,获取对象的键名只能是 stringnumbersymbol

Pick<Type, Keys>

用于构造一个类型,它是从Type类型里面挑了一些属性Keys

ts 复制代码
type OType = {
  name: string,
  age?: number
}
type PickType1 = Pick<OType, 'age'> // type PickType1 = { age?: number | undefined;}

type MyPick<T, K extends keyof T> = {
  [P in K]: T[P]
} 
type PickType2 = MyPick<OType, 'age'> // type PickType2 = { age?: number | undefined;}

Omit<Type, Keys>

用于构造一个类型,它是从Type类型里面过滤了一些属性Keys

  • 思路:
    • 使用 keyof 获取所有的键

    • 使用条件类型过滤掉需要剔除的键

    • 重新构造一个新的类型,包含剩余的键

ts 复制代码
type OType = {
  name: string,
  age?: number
}
type OmitType1 = Omit<OType, 'age'> // type OmitType1 = { name: string; }

type MyOmit<T, K extends keyof T> = {
  [P in keyof T as P extends K ? never : P]: T[P]
}
type OmitType2 = MyOmit<OType, 'age'> // type OmitType2 = { name: string; }
  • keyof T :获取类型 T 的所有键名

  • 条件类型 P extends K ? never : P :如果键 P 是要剔除的键 K,那么在结果中设置为 never,否则保留

Exclude<UnionType, ExcludedMembers>

用于构造一个类型,它是从UnionType联合类型里面排除了所有可以赋给ExcludedMembers的类型

ts 复制代码
type ExcludeType1 = Exclude<'上海' | 'aaa', '上海'> // type ExcludeType1 = "aaa"

type MyExclude<T, K> = T extends K ? never : T
type ExcludeType2 = MyExclude<'上海' | 'aaa', '上海'> // type ExcludeType2 = "aaa"
  • T extends K :判断 T 中的每个成员是否是 K子类型 ,如果成立,就返回 never,否则返回 T

Extract<Type, Union>

用于构造一个类型,它是从Type类型里面提取了所有可以赋给Union的类型

ts 复制代码
type ExtractType1 = Extract<'上海' | 'aaa' | 'bbb', '上海' | 'aaa'> // type ExtractType1 = "aaa" | "上海"

type MyExtract<T, K> = T extends K ? T : never
type ExtractType2 = MyExtract<'上海' | 'aaa' | 'bbb', '上海' | 'aaa'> // type ExtractType2 = "aaa" | "上海"

NonNullable<Type>

用于构造一个类型,这个类型从Type中排除了所有的null、undefined的类型

  • 现象:
    • TypeScript 中,'aaa' 是一个字面量类型 ,而 string 是一个宽泛类型
    • 当你将字面量类型与其宽泛类型(例如 'aaa'string)联合在一起时,TypeScript 会进行类型合并
ts 复制代码
type NonNullableType = NonNullable<any[] | 'aaa' | null | number> // type NonNullableType = number | any[] | "aaa"
type NonNullableType1 = NonNullable<any[] | 'aaa' | null | number | undefined | string> // type NonNullableType = string | number | any[]

type MyNonNullable<T> = T extends null | undefined ? never : T
type NonNullableType2 = MyNonNullable<any[] | 'aaa' | null | number> // type NonNullableType2 = number | any[] | "aaa"
type NonNullableType3 = MyNonNullable<any[] | 'aaa' | null | number | undefined | string> // type NonNullableType3 = string | number | any[]

ReturnType<Type>

用于构造一个含有Type函数的返回值的类型

ts 复制代码
type A = ReturnType<() => string> // string 
type B = ReturnType<() => any[]> // any[] 
type C = ReturnType<() => number | string> // string | number 
type D = ReturnType<number> // never 不是函数类型报错

type MyReturnType<T extends (...args: any[]) => any> = T extends (...args: any[]) => infer R ? R : never 
type A = MyReturnType<() => string> // string 
type B = MyReturnType<() => any[]> // any[] 
type C = MyReturnType<() => number | string> // string | number 
type D = MyReturnType<number> // never 不是函数类型报错

InstanceType<Type>

用于构造一个由所有Type的构造函数的实例类型组成的类型,该工具类型接受一个构造函数类型 Type 作为参数,如果 Type 是合法的构造函数类型 ,它会提取出构造函数返回的实例类型 ,如果不是合法的构造函数类型则返回 any

ts 复制代码
// 创建实例的工具函数时会用到这个InstanceType
function factory1<T extends new (...args: any[]) => any>(
  ctor: T,
  ...args: ConstructorParameters<T> // 使用 ConstructorParameters 来匹配构造函数参数
): InstanceType<T> {
  return new ctor(...args); // 将参数传递给构造函数
}
const person1 = factory1(Person, "Alice", 25); // const person1: Person
const student1 = factory1(Student, "清华"); // const student1: Student


type MyInstanceType<T extends new (...args: any[]) => any> = T extends new (...args: any[]) => infer R ? R : never
function factory2<T extends new (...args: any[]) => any>(
  ctor: T,
  ...args: ConstructorParameters<T> // 使用 ConstructorParameters 来匹配构造函数参数
): MyInstanceType<T> {
  return new ctor(...args); // 将参数传递给构造函数
}
const person2 = factory1(Person, "Alice", 25); // const person2: Person
const student2 = factory1(Student, "清华"); // const student2: Student

类型体操

  • 类型系统其实在很多语言里面都是有的,比如Java、Swift、C++等等,但是相对来说TypeScript的类型非常灵活

  • 因为TypeScript的目的是为JavaScript添加一套类型校验系统,JavaScript本身的灵活性,让TypeScript类型系统不得不增加附加的功能以适配JavaScript的灵活性,所以TypeScript是一种可以支持类型编程的类型系统

  • 如果你在开发一些框架、库,或者通用性的工具,为了考虑各种适配的情况,就需要使用类型编程

  • 很多开发者为了进一步增强自己的TypeScript编程能力,还会专门去做一些类型体操的题目,下面是题目链接可以自行练习:

相关推荐
腾讯TNTWeb前端团队29 分钟前
helux v5 发布了,像pinia一样优雅地管理你的react状态吧
前端·javascript·react.js
uhakadotcom4 小时前
视频直播与视频点播:基础知识与应用场景
后端·面试·架构
范文杰4 小时前
AI 时代如何更高效开发前端组件?21st.dev 给了一种答案
前端·ai编程
拉不动的猪4 小时前
刷刷题50(常见的js数据通信与渲染问题)
前端·javascript·面试
拉不动的猪4 小时前
JS多线程Webworks中的几种实战场景演示
前端·javascript·面试
FreeCultureBoy5 小时前
macOS 命令行 原生挂载 webdav 方法
前端
uhakadotcom5 小时前
快速开始使用 n8n
后端·面试·github
uhakadotcom5 小时前
Astro 框架:快速构建内容驱动型网站的利器
前端·javascript·面试
uhakadotcom5 小时前
了解Nest.js和Next.js:如何选择合适的框架
前端·javascript·面试
uhakadotcom6 小时前
React与Next.js:基础知识及应用场景
前端·面试·github