手牵手带你掌握TS中的泛型编程

225 阅读12分钟

概述

泛型,作为TS语言的重点与难点,是我们必须掌握的知识。那什么是泛型呢?

我看了不少的博客,包括官方文档,大佬的文章等,发现对于泛型都没有下一个准确的定义,只是说“类似”、“型如”之类的字眼,官方描述如下:

1.png

结合官方和各位大佬的描述,我总结的泛型的定义如下:

在TypeScript(TS)中,泛型是一种工具,它允许在定义函数、类、接口或类型别名时使用类型参数

这些类型参数可以在实际使用时被具体的类型所替换,从而使得代码更加灵活和可复用。

泛型的本质是通过类型参数建立函数参数和返回值之间的对应关系,增强代码的复用性。

这么说可能还是有些抽象,话不多说,看代码吧,看看代码就懂了。

泛型语法的基本使用

// 1.理解形参和实例参数化, 但是参数的类型是固定的
// function foo(name: string, age: number) {

// }
// foo("Jasmin", 19)
// foo("kobe", 30)

// 2.定义函数: 将传入的内容返回
// number/string/{name: string}
function bar<Type>(arg: Type): Type {
  return arg
}

// 2.1. 完整的写法
const res1 = bar<number>(123)
const res2 = bar<string>("abc")
const res3 = bar<{name: string}>({ name: "Lee" })

// 2.2. 省略的写法
const res4 = bar("aaaaaaaaa")
const res5 = bar(11111111)

// let message = "Hello World"

export {}

再看张图吧。 在这里插入图片描述

泛型实现类型参数化

这里的T,它是一种类型变量(type variable),它作用于类型,而不是值 这里我们可以使用两种方式来调它:

  • 方式一:通过 <类型 > 的方式 将类型传递给函数;
foo<string>("abc")
foo<number>(123)
  • 方式二:通过类型推导( type argument inference)自动推导出我们传入变量的类型
    • 在这里会推导出它们是字面量类型的,因为字面量类型对于我们的函数也是适用的
foo("abc")
foo(123)

泛型的基本补充

// 元组: useState函数
function useState<Type>(initialState: Type): [Type, (newState: Type) => void] {
  let state = initialState
  function setState(newState: Type) {
    state = newState
  }

  return [state, setState]
}

// 初始化count
const [count, setCount] = useState(100)
const [message, setMessage] = useState("Hello World")
const [banners, setBanners] = useState<any[]>([])

export {}

常见的泛型变量说明

  • T:Type的缩写,类型
  • K、V:key和value的缩写,键值对的缩写
  • E:Element的缩写,元素
  • O:Object的缩写,对象

也可以传入多个泛型变量

function foo<T, E>(arg1: T, arg2: E) {

}

foo(10, 20)
foo(10, "abc")
foo<string, { name: string }>("abc", { name: "why" })

export {}

泛型接口与泛型类

泛型接口

// 可以在定义泛型变量时指定一个默认值
interface IKun<Type = string> {
  name: Type
  age: number
  slogan: Type
}

const kunkun: IKun<string> = {
  name: "Lee",
  age: 18,
  slogan: "哈哈哈"
}

const ikun2: IKun<number> = {
  name: 123,
  age: 20,
  slogan: 666
}

const ikun3: IKun = {
  name: "kun",
  age: 30,
  slogan: "哭唧唧,我再也不能给你们唱跳rap了"
}

export {}

泛型类

class Point<Type = number> {
  x: Type
  y: Type
  constructor(x: Type, y: Type) {
    this.x = x
    this.y = y
  }
}

const p1 = new Point(10, 20)
console.log(p1.x)
const p2 = new Point("123", "321")
console.log(p2.x)

export {}

泛型约束

有时我们希望传入的类型有某些共性,但是这些共性可能不是在同一种类型中。比如 string 和array都是有 length属性的,或者某些对象也是会有length属性的。那么只要是拥有length 属性的都可以作为我们参数类型,那么应该如何操呢? 这就要引入我们所说的泛型约束了。看例子吧

interface ILength {
  length: number
}

// 1.getLength没有必要用泛型
function getLength(arg: ILength) {
  return arg.length
}

const length1 = getLength("aaaa")
const length2 = getLength(["aaa", "bbb", "ccc"])
const length3 = getLength({ length: 100 })


// 2.获取传入的内容, 这个内容必须有length属性
// Type相当于是一个变量, 用于记录本次调用的类型
// 所以在整个函数的执行周期中, 一直保留着参数的类型

// 这里表示是传入的类型必须有length这个属性,也可以有其他属
// 但是必须至少有这个成员
function getInfo<Type extends ILength>(args: Type): Type {
  return args
}

const info1 = getInfo("aaaa")
const info2 = getInfo(["aaa", "bbb", "ccc"])
const info3 = getInfo({ length: 100 })

// getInfo(12345)
// getInfo({})

export {}

泛型参数使用约束

在泛型约束中使用类型参数(Using Type Parameters in Generic Constraints)。你可以声明一个类型参数,这个类型参数被其他类型参数约束。

举个栗子:我们希望获取一对象给定属性名的值

  1. 我们需要确保不会获取 obj上不存在的属性
  2. 所以我们要在两个类型之间建立一个约束
// 传入的key类型, obj当中key的其中之一
interface IKun {
  name: string
  age: number
}

// keyof:映射类型语法,可以理解为联合类型的语法糖
// 下一个知识点就会对其进行讲述
type IKunKeys = keyof IKun // "name"|"age"

function getObjectProperty<O, K extends keyof O>(obj: O, key: K){
  return obj[key]
}

const info = {
  name: "why",
  age: 18,
  height: 1.88
}

const name = getObjectProperty(info, "name")

export {}

映射类型

有的时候,一个类型需要基于另外一个类型,但是我们又不想拷贝一份,这个时候可以考虑使用映射类型

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

映射类型建立在索引签名的语法上:

  • 映射类型,就是使用了 PropertyKeys联合类型的泛型
  • 其中 PropertyKeys多是通过keyof创建,然后循环遍历键名创建一个类型

需要注意的是 映射类型不能使用interface定义

基本使用

// TypeScript提供了映射类型: 函数
// 映射类型不能使用interface定义
// Type = IPerson
// keyof = "name" | "age"
type MapPerson<Type> = {
  // 索引类型以此进行使用
  [aaa in keyof Type]: Type[aaa]

  // name: string
  // age: number
}


interface IPerson {
  name: string
  age: number
}

// 拷贝一份IPerson
// interface NewPerson {
//   name: string
//   age: number
// }
type NewPerson = MapPerson<IPerson>

export {}

映射修饰符

在使用映射类型时,有两个额外的修饰符可能会到:

  • 一个是 readonly ,用于设置属性只读
  • 另一个是 ? ,用于设置属性可选

修饰符符号: - 或 +

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

type MapPerson<Type> = {
  // 这个映射表达的意思是:
  //   1.首先,使用映射修饰符readonly和?将Type中的所有属性设置为只读并且可选的
  //   2.然后,又使用修饰符符号 - 将这两个修饰符删除
  -readonly [Property in keyof Type]-?: Type[Property]
}

/* 
  不知道大家会不会有疑问,先添加,后删除,那这不是脱裤子放P -- 多次一举吗?
   这不是跟 [Property in keyof Type]: Type[Property] 表达的意思一样吗?
   事实上,不是这样的,看下面这个例子你就懂了。
   经过MapPerson里的映射`运算`,类型IPerson的所有属性都变为必填,并且可读可写的。
   而如果是经过这个: [Property in keyof Type]: Type[Property]
   没有任何修饰符和修饰符符号的`运算`,IPerson的各个属性还会保持他们原先的属性。
   即name,age,address都是可读写的,但是age和address是可选的
   height是只读的
 */

interface IPerson {
  name: string
  age?: number
  readonly height: number
  address?: string
}

type IPersonRequired = MapPerson<IPerson>

const p: IPersonRequired = {
  name: "why",
  age: 18,
  height: 1.88,
  address: "广州市"
}

export {}

内置工具和类型体操

这部分是难点,实在掌握不了可以以后慢慢学。

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

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

条件类型

很多时候,日常开发中我们需要基于输入的值来决定输出的值,同样我们 也需要基于输入的值的类型来决定输出的值的类型

条件类型( Conditional types)就是用来帮助我们描述输入类型和输出类型之间的关系。

条件类型的写法有点类似于 JavaScript中的条件表达式( condition ? trueExpression : falseExpression)

SomeType extends OtherType ? TrueType : FalseType

type IDType = number | string

// 判断number是否是extends IDType
// const res = 2 > 3? true: false
type ResType = boolean extends IDType? true: false

// 举个栗子: 函数的重载
// function sum(num1: number, num2: number): number
// function sum(num1: string, num2: string): string

// 错误的做法: 类型扩大化
// function sum(num1: string|number, num2: string|number): string

function sum<T extends number | string>(num1: T, num2: T): T extends number ? number : string
function sum(num1, num2) {
  return num1 + num2
}

const res = sum(20, 30)
const res2 = sum("abc", "cba")
// const res3 = sum(123, "cba")

export {}

条件类型的类型推断infer

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

type CalcFnType = (num1: number, num2: string) => number

function foo() {
  return "abc"
}

// 总结类型体操题目: MyReturnType
type MyReturnType<T extends (...args: any[]) => any > 
	= T extends (...args: any[]) => infer R ? R: never

type MyParameterType<T extends (...args: any[]) => any > 
	= T extends (...args: infer A) => any ? A: never

// 获取一个函数的返回值类型: 内置工具
type CalcReturnType = MyReturnType<CalcFnType>
type FooReturnType = MyReturnType<typeof foo>
// type FooReturnType2 = MyReturnType<boolean>

type CalcParameterType = MyParameterType<CalcFnType>

export {}

分发条件类型

当在泛型中使用条件类型的时候,如果传入一个联合类型,就会变成分发的(distributive)

type toArray<Type> = Type extends any ? Type[] : never

// string[] | number[]
type newType = toArray<string | number>

如果我们向 ToArray 传入一个联合类型,这个条件类型会被应用到联合类型的每个成员:

  1. 当传入string | number 时,会遍历联合类型中的每一个成员
  2. 相当于ToArray<string> | ToArray<number>
  3. 所以最后的结果是: string[] | number[]

常见的内置类型

一下所举的例子都是内置类型的自我实现,也就是类型体操。

Partial<Type>

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

interface IKun {
  name: string
  age: number
  slogan?: string
}

// 类型体操
type MYPartial<T> = {
  [P in keyof T]?: T[P] 
}

// IKun都变成可选的
type IKunOptional = MYPartial<IKun>

export {}

Required<Type>

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

interface IKun {
  name: string
  age: number
  slogan?: string
}

// 类型体操
type MYRequired<T> = {
  [P in keyof T]-?: T[P] 
}

// IKun都变成可选的
type IKun2 = MYRequired<IKun>

export {}

Readonly<Type>

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

interface IKun {
  name: string
  age: number
  slogan?: string
}

// 类型体操
type MYReadonly<T> = {
  readonly [P in keyof T]: T[P] 
}


// IKun都变成可选的
type IKun2 = MYReadonly<IKun>

export {}

Record<Keys, Type>

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

interface IKun {
  name: string
  age: number
  slogan?: string
}

// 类型体操
// name | age | slogan
type keys = keyof IKun
type Res = keyof any // => number|string|symbol

// 确实keys一定是可以作为key的联合类型
type MYRecord<Keys extends keyof any, T> = {
  [P in Keys]: T
}

// IKun都变成可选的
type t1 = "上海" | "北京" | "洛杉矶"
type IKuns = MYRecord<t1, IKun>

const ikuns: IKuns = {
  "上海": {
    name: "xxx",
    age: 10
  },
  "北京": {
    name: "yyy",
    age: 5
  },
  "洛杉矶": {
    name: "zzz",
    age: 3
  }
}

export {}

Pick<Type, Keys>

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

interface IKun {
  name: string
  age: number
  slogan?: string
}

// 确实keys一定是可以作为key的联合类型
type MYPick<T, K extends keyof T> = {
  [P in K]: T[P]
}

// IKun都变成可选的
type IKuns = MYPick<IKun, "slogan"|"name">

export {}

Omit<Type, Keys>

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

interface IKun {
  name: string
  age: number
  slogan?: string
}

// 确实keys一定是可以作为key的联合类型
type MYOmit<T, K extends keyof T> = {
  [P in keyof T as P extends K ? never: P]: T[P]
}

// IKun都变成可选的
type IKuns = MYOmit<IKun, "slogan"|"name">

export {}

Exclude< UnionType, ExcludedMembers>

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

type IKun = "sing" | "dance" | "rap"

// 确实keys一定是可以作为key的联合类型
type MYExclude<T, E> = T extends E? never: T

// IKun都变成可选的
type IKuns = MYExclude<IKun, "rap">

export {}

有了 MYExclude,我们可以使用它来实现MYOmit

Extract< Type, Union>

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

type IKun = "sing" | "dance" | "rap"

// 确实keys一定是可以作为key的联合类型
type MYExtract<T, E> = T extends E? T: never

// IKun都变成可选的
type IKuns = MYExtract<IKun, "rap"|"dance">

export {}

NonNullable< Type >

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

type IKun = "sing" | "dance" | "rap" | null | undefined

// 确实keys一定是可以作为key的联合类型
type MYNonNullable<T> = T extends null|undefined ? never: T

// IKun都变成可选的
type IKuns = MYNonNullable<IKun>

export {}

ReturnType<Type>

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

// Construct a type consisting of the return type of function type
// 第一个extends是对传入条件进行限制
// 第二个extends是为了进行条件获取类型
type MYReturnType<T extends (...args: any) => any >
	= T extends (...args: any) => infer R ? R : never

type T1 = MYReturnType<() => string>
type T2 = MYReturnType<() => void>
type T3 = MYReturnType<(num1: number, num2: number) => string>

function sum(num1: number, num2: number) {
  return num1 + num2
}

function getInfo(info: { name: string, age: number }) {
  return info.name + info.age
}

type T4 = MYReturnType<typeof sum>
type T5 = MYReturnType<typeof getInfo>

InstanceType< Type>

用于构造一个由所有Type的构造函数的实例类型组成的类型

class Person {}
class Dog {}


// 类型体操
type HYInstanceType<T extends new (...args: any[]) => any>
	 = T extends new (...args: any[]) => infer R? R: never


const p1: Person = new Person()

// typeof Person: 构造函数具体的类型
// InstanceType构造函数创建出来的实例对象的类型
type HYPerson = HYInstanceType<typeof Person>
const p2: HYPerson = new Person()


// 构造函数的例子
// 通过的创建实例的工具函数时会用到这个InstanceType
function factory<T extends new (...args: any[]) => any>(ctor: T): HYInstanceType<T> {
  return new ctor()
}

const p3 = factory(Person)
const d = factory(Dog)

export {}

总结