彻底理解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编程能力,还会专门去做一些类型体操的题目,下面是题目链接可以自行练习:

相关推荐
青灯文案16 分钟前
前端 HTTP 请求由 Nginx 反向代理和 API 网关到后端服务的流程
前端·nginx·http
ThisIsClark9 分钟前
【后端面试总结】MySQL主从复制逻辑的技术介绍
mysql·面试·职场和发展
m0_7482548811 分钟前
DataX3.0+DataX-Web部署分布式可视化ETL系统
前端·分布式·etl
ZJ_.22 分钟前
WPSJS:让 WPS 办公与 JavaScript 完美联动
开发语言·前端·javascript·vscode·ecmascript·wps
GIS开发特训营26 分钟前
Vue零基础教程|从前端框架到GIS开发系列课程(七)响应式系统介绍
前端·vue.js·前端框架·gis开发·webgis·三维gis
Cachel wood1 小时前
python round四舍五入和decimal库精确四舍五入
java·linux·前端·数据库·vue.js·python·前端框架
学代码的小前端1 小时前
0基础学前端-----CSS DAY9
前端·css
joan_851 小时前
layui表格templet图片渲染--模板字符串和字符串拼接
前端·javascript·layui
程序猿进阶1 小时前
深入解析 Spring WebFlux:原理与应用
java·开发语言·后端·spring·面试·架构·springboot
m0_748236111 小时前
Calcite Web 项目常见问题解决方案
开发语言·前端·rust