Skip to content

一. 泛型语法的基本使用

前言

在软件开发中,我们往往希望代码能够重用、灵活,这样不仅使得代码更易于维护,还能适应更多的场景。 我们可以通过函数来封装代码逻辑,并通过传递不同的参数,让同一个函数实现不同的功能。 然而,对于参数的类型能不能也具备灵活性呢?

认识泛型(Generics)

引言

泛型正是解决这种需求的工具。泛型让我们可以在函数、类或接口中使用类型参数,从而增强代码的重用性类型安全性

什么是类型的参数化?

举个简单的需求:我们希望实现一个函数,这个函数接收一个参数并返回这个参数本身。为了确保类型一致,我们希望参数类型和返回类型一致,并且这个类型是可以变化的。

typescript
function foo(arg: number): number {
  return arg
}

const res1 = foo(10) // 返回值是 number 类型

这个代码可以运行,但只能用于 number 类型,换成其他类型(如 string 或 boolean)就会报错。例如:

typescript
const res2 = foo('hello') // Error:类型 “string” 不能赋给类型 “number”

为什么需要泛型?

如果要适应不同的类型,是否可以考虑使用联合类型或 any 类型:

使用联合类型(number | string)虽然可以接收不同的类型,但会丢失精确的类型信息,如下所示:

typescript
function foo(arg: number | string): number | string {
  return arg
}
  
const res1 = foo(10)        // res1 的类型是 number | string
const res2 = foo('hello')   // res2 的类型是 number | string

无论传入 number 还是 string,返回值的类型都是 number | string,我们无法获知具体的返回类型。

使用 any 类型:虽然能够避免类型报错,但同样会丢失类型信息,导致返回值也是 any 类型:

typescript
function foo(arg: any): any {
  return arg
}
  
const res1 = foo(10)       // res1 是 any 类型
const res2 = foo('hello')  // res2 也是 any 类型

泛型实现类型参数化

引言

泛型可以解决上述问题。使用泛型时,我们通过一种特殊的类型变量(Type Variable)来捕获参数的类型信息,并使返回值和参数保持一致。这个类型变量可以在调用时动态地确定

typescript
function foo<Type>(arg: Type): Type {
  return arg
}

这里定义了一个泛型函数 foo,类型参数 Type 是在运行时动态确定的。调用时可以用两种方式传递类型:

  • 方式一:通过<类型>的方式将类型显式传递
    typescript
    const res1 = foo<string>('hello')  // 返回值 res1 的类型是 string
    const res2 = foo<number>(123)      // 返回值 res2 的类型是 number
  • 方式二:通过隐式的类型推导,自动推导出传入变量的类型:
    typescript
    const res3 = foo('world') // 返回值 res3 的类型是 'world'(字面量类型)
    const res4 = foo(456)     // 返回值 res4 的类型是 456(字面量类型)
        
    // 如果使用的是 let,自动推导出来的是通用类型,而非字面量类型
    let res5 = foo('ccc')     // 返回值 res5 的类型为 string

总结

  • 通过显式传递类型,推导出来的是显式传递的类型。
  • 通过隐式类型推导const 推导出来的是字面量类型,而 let 推导出来的是通用类型

泛型的多参数和常用参数名称

引言

泛型的强大之处在于它允许我们在函数、接口和类中定义多个类型参数,满足不同需求。

例如:

typescript
function pair<T, U>(first: T, second: U): [T, U] {
  return [first, second]
}

const result = pair<string, number>('hello', 42) // 返回值 result 的类型是 [string, number]

在日常开发中,通常使用以下字母来表示不同的类型参数:

typescript
/**
     T:   表示通用类型(Type)
     K、V:表示键值对中的键(Key)和值(Value)
     E:   表示元素类型(Element)
     O:   表示对象类型(Object)
 */

二. 泛型在接口与类中的使用

前言

在 TypeScript 中,泛型不仅可以在函数中使用,也可以用于接口和类。通过泛型,接口和类可以接收不同的类型参数,这样它们可以灵活地适配多种类型。

泛型接口

引言

在接口中使用泛型可以让接口定义更灵活。

示例:

定义一个接口 IKun,允许类型参数用于其中的属性和方法。

typescript
// T 表示接受通用类型,默认为 string 类型
interface IKun<T = string> {
  name: T                   // 可以是任何类型 T
  age: number               // 固定为 number 类型
  slogan: T[]               // 包含多个 T 类型的数组
  dance: (value: T) => void // 接收 T 类型参数的函数
}

// 使用泛型接口定义不同的对象
const ikun1: IKun<string> = {
  name: 'KunKun',                     // name 为 string 类型
  age: 18,
  slogan: ['Hey!', 'Let\'s dance!'],  // string 数组
  dance: function(value: string) {
    console.log("舞蹈口号:" + value) 
  }
}

const ikun2: IKun<number> = {
  name: 42,                 // name 为 number 类型
  age: 20,
  slogan: [100, 200],       // number 数组
  dance: function(value: number) {
    console.log("舞蹈节拍:" + value)
  }
}

// 不指定类型参数时,T 将默认为 string
const ikun3: IKun = {
  name: 'Lara',
  age: 25,
  slogan: ['Let\'s go!'],
  dance: function(value) {
    console.log("默认舞蹈:" + value)
  }
}

上面示例中,IKun 接口使用了泛型 T,因此可以为 name、slogan 和 dance 的参数指定不同的类型。
通过这种方式,接口的类型变得更加灵活,可以适应更多种类的数据。

泛型类

引言

与接口类似,类也可以使用泛型来适应不同类型的数据。
定义一个泛型类时,可以使用类型参数让类在创建对象时指定属性的类型。

示例:

定义一个 Point 类,表示二维坐标中的一个点,不仅可以使用 number,还可以使用 string、boolean 等任意类型来表示。

typescript
// 使用泛型 T 定义一个坐标类,默认为 number 类型
class Point<T = number> {
  constructor(public x: T, public y: T) {} // x 和 y 是类型 T

  displayPoint() {
    console.log(`坐标: (${this.x}, ${this.y})`);
  }
}

// 1. 使用默认的 number 类型
const point1 = new Point(10, 20);
point1.displayPoint(); // 输出: 坐标: (10, 20)

// 2. 使用 string 类型
const point2 = new Point<string>('10px', '20px');
point2.displayPoint(); // 输出: 坐标: (10px, 20px)

// 3. 使用 boolean 类型
const point3 = new Point<boolean>(true, false);
point3.displayPoint(); // 输出: 坐标: (true, false)

在这个例子中,Point 类通过泛型参数 T 支持多种类型。
实例化类时,可以指定 T 的类型,也可以省略不写,这时 T 默认为 number 类型。
使用泛型类时,不仅可以存储数值坐标,还可以表示字符串坐标、布尔值等。

更多练习:泛型的应用

下面再给出几个典型的泛型接口和类的应用案例,帮助深入理解。

  1. 泛型接口示例:数据包装器

    创建一个数据包装接口,包含一个泛型类型 T 的数据和一个 status 字段:

    typescript
    interface DataWrapper<T> {
      data: T
      status: 'loading' | 'success' | 'error'
    }
    
    const wrappedString: DataWrapper<string> = {
      data: 'hello',
      status: 'success'
    }
    
    const wrappedNumberArray: DataWrapper<number[]> = {
      data: [1, 2, 3],
      status: 'loading'
    }
  2. 泛型类示例:简单的存储容器

    定义一个泛型类 Container,用于存储不同类型的数据,并提供一个 get 方法返回数据。

    typescript
    class Container<T> {
      constructor(private content: T) {}
      
      getContent(): T {
        return this.content
      }
    }
    
    const stringContainer = new Container<string>('Hello')
    console.log(stringContainer.getContent()) // 输出: Hello
    
    const numberContainer = new Container<number>(42)
    console.log(numberContainer.getContent()) // 输出: 42

总结

泛型接口和泛型类让我们可以编写更具通用性、可扩展性和复用性的代码。在开发中,合理使用泛型可以帮助我们构建出灵活而稳定的接口和类,实现更清晰的类型约束。

三. 泛型约束 和 类型条件

前言

在泛型编程中,有时我们会希望泛型能够满足某些特定条件或特性,比如某些属性的存在。通过泛型约束和类型条件,我们可以在泛型中加入一些限制条件,让代码更灵活、更安全。

泛型约束一(Generic Constraints)

需求背景:有时,我们希望传入的类型具有某些特定的属性,而不仅仅是任意类型。例如,string 和 array 类型都具有 length 属性,但 number 类型没有。那么如何限制泛型参数的类型,以确保其包含 length 属性呢?

解决方式: 我们可以定义一个接口 ILength,只包含 length 属性。然后,通过让泛型参数继承 ILength,就可以确保泛型类型包含 length 属性。

示例

typescript
// 1. 定义包含 length 属性的接口
interface ILength {
  length: number
}

// 2. 不使用泛型的情况
function getLength(arg: ILength): number {
  return arg.length
}

const length1 = getLength("hello")             // string 类型
const length2 = getLength(["one", "two"])      // array 类型
const length3 = getLength({ length: 42 })      // 对象类型


// 3. 使用泛型约束,确保传入的类型包含 length 属性
function getInfo<T extends ILength>(args: T): T {
  return args
}

const info1 = getInfo("world")                        // string 类型
const info2 = getInfo(["apple", "banana"])            // array 类型
const info3 = getInfo({ length: 100, extra: "info" }) // 对象类型,包含其他属性也可

// 错误示例(因为缺少 length 属性):
getInfo(12345)  // Error:类型 "12345" 中缺少属性 "length",但类型 "ILength" 中需要该属性
getInfo({})     // Error:类型 "{}" 中缺少属性 "length",但类型 "ILength" 中需要该属性

通过 T extends ILength,我们要求传入的类型 T 必须有 length 属性,同时可以包含其他属性。这样,即使 T 是一个包含 length 的对象类型,也可以正常工作,而不符合条件的类型则会报错。

泛型约束二(在泛型约束中使用类型参数)

需求背景:在泛型中,我们可以使用一个类型参数去约束另一个类型参数。这在我们需要根据第一个参数的属性来限制第二个参数时非常有用。

例如,我们希望获取一个对象中的特定属性值,但又要确保这个属性在对象中存在。这就需要使用泛型约束来验证属性是否存在。

示例:假设我们有一个接口 IKun,包含 name 和 age 两个属性。我们希望写一个函数 getObjectProperty,用于获取对象中指定的属性值,但同时确保属性是对象实际包含的。

typescript
// 1. 定义接口 IKun
interface IKun {
  name: string
  age: number
}

// 2. 使用 keyof 获取 IKun 接口所有属性名组成联合类型
type IKunKeys = keyof IKun // 'name' | 'age'

// 3. 定义一个泛型函数,其中第二个类型参数 k 被约束为第一个类型参数 O 的属性名
function getObjectProperty<O, K extends keyof O>(obj: O, key: K): O[K] {
  return obj[key]
}

// 示例对象
const info = {
  name: 'KunKun',
  age: 18,
  height: 1.88
}

// 4. 调用函数
const info_name = getObjectProperty(info, 'name') // 正确,返回 string 类型
const info_age = getObjectProperty(info, 'age')   // 正确,返回 number 类型

// 错误示例(因为属性不存在):
getObjectProperty(info, 'address') // Error:属性 "address" 不存在于类型 "IKun"
  • K extends keyof O:表示 K 必须是 O 的属性名之一。keyof O 提取对象类型 O 的所有键,并将其组成一个联合类型。
  • O[K]:表示返回 obj 中 key 对应的属性值。

四. keyof 操作符和映射类型

keyof 操作符

这是一个 TypeScript 的关键字,用来获取某个类型的所有属性名组成的字符串字面量联合类型

示例一:提取对象的键``

typescript
type Person = {
  name: string
  age: number
  isActive: boolean
}

type PersonKeys = keyof Person  // 'name' | 'age' | 'isActive'

在这个例子中,keyof Person 提取出 Person 类型的所有键,PersonKeys 的类型是一个联合类型,即 'name' | 'age' | 'isActive'。

示例二:数字键的情况

typescript
type MyObject = {
  0: string
  1: number
}

type MyObjectKeys = keyof MyObject  // '0' | '1'

这里 keyof MyObject 会返回 '0' | '1',即使对象的键是数字类型,keyof 返回的结果仍然是字符串字面量类型。

示例三:keyof any

在 TypeScript 中,keyof any 是一个非常特殊的类型,它表示所有 JavaScript 对象有效的 key 类型组成的联合类型

typescript
type Keys = keyof any  // => number | string | symbol

为什么是 number | string | symbol?

  • string:所有字符串类型的键,例如 "name"、"age"。
  • number:数字类型的键,实际上 JavaScript 对象中的数字键会被自动转换成字符串(例如,{ 1: "one" },其实是 {"1": "one"})。
  • symbol:符号类型的键,用于唯一标识对象属性。

所以,keyof any 可以表示任何类型的对象的键,不限于特定类型的对象。

举个例子:

typescript
type Keys = keyof any // => number | string | symbol

const key1: Keys = "a"      // 合法,"a" 是 string 类型
const key2: Keys = 123      // 合法,123 是 number 类型
const key3: Keys = Symbol() // 合法,Symbol 是 symbol 类型

总结

keyof any 等于 number | string | symbol,它是所有 JavaScript 对象键的类型组成的联合类型。

映射类型(Mapped Types)

在 TypeScript 中,映射类型是一种可以基于已有类型创建新类型的方式。这些新类型具有与原类型相同的属性结构,但可以对每个属性进行一些修改(如设置为可选或只读)。这种方式避免了重复定义类型,增加了代码的灵活性和可维护性。

作用与特点

  • 复用性高:通过映射类型,我们可以轻松创建基于其他类型的变体,而不需要重复定义。
  • 灵活性强:映射类型可以结合修饰符实现多种变体(如将所有属性设置为可选或只读)。
  • 核心原理:映射类型是通过 keyof 操作符生成的联合类型来遍历属性的。

基础语法
映射类型基于索引签名和 keyof 关键字:

typescript
type MapType<T> = {
  [P in keyof T]: T[P]
}
  • keyof:基于对象类型 T 的所有属性名生成一个联合类型。
  • P in keyof T:使用 in 遍历联合类型中的每个属性。
  • T[P]:通过索引 P 访问对象 T 的属性值。

示例
假设我们有一个表示 IPerson 的接口:

typescript
interface IPerson {
  name: string
  age: number
}

// 创建一个映射类型 MapPerson,用于复制 IPerson 的结构
type MapPerson<T> = {
  [Property in keyof T]: T[Property]
}

// 使用映射类型创建一个拷贝 Person 的类型
type NewPerson = MapPerson<IPerson>

在这里,NewPerson 将完全复制 IPerson 的结构,因此 NewPerson 等价于:

typescript
type NewPerson = {
  name: string
  age: number
}

映射类型提供了一种灵活的方式来创建新类型,让我们可以在不复制代码的情况下对已有类型进行结构性变更。它为类型的复用和扩展带来了很大的便利。

⚠️ 注意

映射类型不支持使用 interface 来定义

映射修饰符(Mapping Modifiers)

在映射类型中,我们可以使用一些修饰符来控制属性的特性,比如将属性设置为只读(readonly)或可选(?)。

常用映射修饰符

  • readonly:设置属性为只读,使属性不可修改。
  • ?(可选):设置属性为可选,使属性在对象中可以省略。

修改符的前缀

  • 前缀 +:为属性添加修饰符(如 +readonly 表示将属性设为只读)。
  • 前缀 -:移除属性的修饰符(如 -readonly 表示移除只读修饰符)。

⚠️ 注意

不写前缀时,默认为 +。

示例
假设我们希望将 IPerson 的所有属性都设置为可选和非只读:

typescript
type MapPerson<T> = {
  -readonly [P in keyof T]?: T[P]
}

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

// 使用映射类型创建 NewPerson 类型,所有属性变为可选且非只读
type NewPerson = MapPerson<IPerson>

const p: NewPerson = {} // 可选属性可以不传
p.name = "Kun"          // 原来的 readonly 属性现在可修改
p.age = 25

在此示例中:

  • -readonly:移除了原属性的 readonly 修饰符,因此 NewPerson 类型中的所有属性都是可修改的。
  • ?:将属性设置为可选,因此我们可以不传入任何属性。

详细案例:多种组合使用
假设我们想创建以下两种映射类型:

  1. 将所有属性变成只读的。
  2. 将所有属性变成必填且可修改的。

可以分别使用如下代码:

typescript
// 1. 全部属性只读
type ReadonlyPerson<T> = {
  readonly [Property in keyof T]: T[Property]
}

// 2. 全部属性必填且可修改
type RequiredPerson<T> = {
  -readonly [Property in keyof T]-?: T[Property]
}

interface IPerson {
  name: string
  age?: number
}

type ReadonlyPersonType = ReadonlyPerson<IPerson>
type RequiredPersonType = RequiredPerson<IPerson>

const p1: ReadonlyPersonType = { name: "Kun", age: 18 }
p1.name = "Later" // 报错:因为 name 是 readonly

const p2: RequiredPersonType = { name: "Kun", age: 18 } // 必填属性,不能省略 age

五. 条件类型

条件类型(Conditional Types)

什么是条件类型?
条件类型允许你根据某个类型是否满足特定条件来选择不同的类型。它的语法类似于条件表达式:

typescript
SomeType extends OtherType ? TrueType : FalseType
  • SomeType extends OtherType:检查 SomeType 是否可以赋值给 OtherType。
  • TrueType:如果条件为真,选择的类型。
  • FalseType:如果条件为假,选择的类型。

示例
假设我们想创建一个函数,根据输入参数的类型返回不同的类型:

typescript
// 定义条件类型的泛型函数签名
function sum<T extends number | string>(num1: T, num2: T): T extends number ? number : string

// 实现函数
function sum(num1: any, num2: any): any {
  return num1 + num2
}

// 调用示例
const res1 = sum(20, 30)        // res1 的类型为 number
const res2 = sum("abc", "cba")  // res2 的类型为 string
const res3 = sum(123, "cba")    // Error:类型“string”不能赋给类型“number”

解析

  • 泛型约束:<T extends number | string> 限制了泛型 T 只能是 number 或 string。
  • 条件类型:T extends number ? number : string 根据 T 是否是 number 来决定返回类型是 number 还是 string。
  • 函数实现:实际函数体不需要关心类型,只需返回 num1 + num2。

在条件类型中使用 infer 推断类型

infer 是 TypeScript 提供的一个关键字,用于在条件类型中推断类型变量。它允许你从一个类型中提取出部分类型,并在条件类型的结果中使用这些推断出的类型。

示例:提取函数的返回类型
假设我们想创建一个类型工具,用于提取函数的返回类型:

typescript
// 定义一个函数类型
type CalcFnType = (num1: number, num2: string) => number

// 定义条件类型,使用 infer 提取返回类型
type MyReturnType<T extends (...args: any[]) => any> = T extends (...args: any[]) => infer R ? R : never

// 使用条件类型提取返回类型
type CalcReturnType = MyReturnType<CalcFnType> // number

// 定义另一个函数
function foo() {
  return "abc"
}

type FooReturnType = MyReturnType<typeof foo> // string

// 错误示例:传入不符合函数类型的类型
type FooReturnType2 = MyReturnType<boolean> // 报错:类型“boolean”不满足约束“(...args: any[]) => any”

解析

  • 类型约束:T extends (...args: any[]) => any 确保 T 是一个函数类型。
  • 推断返回类型:infer R 提取函数的返回类型,并将其赋值给 R。
  • 条件判断:如果 T 是一个函数类型,返回推断出的 R 类型;否则返回 never。

示例:提取函数的参数类型
同样,我们可以创建一个类型工具来提取函数的参数类型:

typescript
// 定义一个函数类型
type CalcFnType = (num1: number, num2: string) => number

// 定义另一个函数
function foo() {
  return "abc"
}

// 定义条件类型,使用 infer 提取参数类型
type MyParameterType<T extends (...args: any[]) => any> = T extends (...args: infer A) => any ? A : never

// 使用条件类型提取参数类型
type CalcParameterType = MyParameterType<CalcFnType> // [num1: number, num2: string]
type FooParameterType = MyParameterType<typeof foo>  // []

条件类型分发(Distributive Conditional Types)

什么是分发条件类型?

当在泛型中使用条件类型时,如果传入的是一个联合类型,条件类型会分发到联合类型的每个成员上

这意味着条件类型会针对联合类型中的每个成员单独应用,最终结果是各个应用结果的联合。

示例

typescript
// 定义一个条件类型,将类型转换为数组
type ToArray<T> = T extends any ? T[] : never

// 使用条件类型转换联合类型,得到 number[] | string[] 而不是 (number|string)[]
type newType = ToArray<number | string> // number[] | string[]
type newType2 = ToArray<number>         // number[]
type newType3 = ToArray<string>         // string[]

解析

  1. 传入联合类型:number | string
  2. 条件类型分发
typescript
ToArray<number | string>
// 等价于:
number extends any ? number[] : never => number[]
string extends any ? string[] : never => string[]
  1. 结果:number[] | string[]

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

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

🔔 提示

条件类型中(T extends E ? X : Y),extends 是 逐个成员独立处理的,尤其是当 T 是联合类型时,T 中的每个成员都会被与 E 进行比较。

普通类型比较中(T extends U),extends 是 整体类型兼容性判断,不是逐个成员检查。对于联合类型 T,它会判断 T 是否可以赋值给 U,而不逐个检查联合类型的每个成员。

实际应用

分发条件类型在处理联合类型时非常有用,特别是在创建通用的类型转换工具时。

示例一:结合分发条件类型和 infer

创建一个类型工具,用于判断一个类型是否是数组,并提取数组元素类型:

typescript
// 判断类型是否为数组,如果是,提取元素类型
type ElementType<T> = T extends (infer E)[] ? E : T

// 使用示例
type ET1 = ElementType<number[]>  // number
type ET2 = ElementType<string>    // string
type ET3 = ElementType<boolean[]> // boolean

示例二:过滤掉 null 和 undefined

条件类型可以用来实现过滤。例如,定义一个 NonNullable<T> 类型,过滤掉 null 和 undefined。

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

// A 的类型为 string | number
type A = NonNullable<string | number | null | undefined>

六. 类型工具 和 类型体操

内置工具和类型体操

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

引言

这是因为 TS 的目的是为 JS 添加一套类型校验系统,因为 JS 本身的灵活性,也让 TS 类型系统不得不增加更多功能以适配 JS 的灵活性。
所以在 TypeScript 中,类型系统是非常强大的,允许你通过类型编程来提升代码的灵活性和可维护性。
这种类型编程系统为 TS 增加了很大的灵活度,同时也增加了它的难度:

如果你仅仅是在开发业务的时候,为自己的 JS 代码增加上类型约束,那么基本不需要太多的类型编程能力。
但是如果你在开发一些框架、库,或者通用性的工具,为了考虑各种适配的情况,就需要使用类型编程。
TS 本身为我们提供了类型工具,帮助我们辅助进行类型转换(前面有用过关于 this 的类型工具)。

很多开发者为了进一步增强自己的 TS 编程能力,还会专门去做一些类型体操的题目:

我们会学习 TS 的编程能力的语法,并且通过学习built-in 类型工具来练习一些类型体操的题目。

Partial<Type>

定义

返回一个与原类型相同,属性都是可选的新类型(属性的值可以为 undefined)。

使用场景

常用于需要部分更新对象的场景,尤其在处理表单数据或做接口请求时非常有用。

示例一: 使用内置的 Partial 工具类型

typescript
interface Person {
  name: string
  age: number
  slogan?: string
}

type PersonOptional = Partial<Person> 

// 等价于:
type PersonOptionalManual = { 
  name?: string
  age?: number
  slogan?: string
} 

示例二: 用类型体操实现一个自己的 Partial 工具类型

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

type MyPartial<T> = {
  [P in keyof T]?: T[P]
}

// 所有属性设为可选
type IKunOptional = MyPartial<IKun>

Required<Type>

定义

与 Partial 作用相反,返回一个与原类型相同,属性都是必填的新类型(属性的值不能为 undefined)。

使用场景

这种类型可以用于确保对象在某些情境下必须包含所有字段。

示例一: 使用内置的 Required 工具类型

typescript
interface Person {
  name: string
  age: number
  slogan?: string
}

type PersonRequired = Required<Person> 

// 等价于:
type PersonRequiredManual = { 
  name: string
  age: number
  slogan: string
} 

示例二: 用类型体操实现一个自己的 Required 工具类型

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

type MyRequired<T> = {
  [P in keyof T]-?: T[P]
}

// 所有属性设为必填
type IKunRequired = MyRequired<IKun>

Readonly<Type>

定义

返回一个与原类型相同,属性都是只读的新类型。

使用场景

常用于确保对象一旦创建后不可修改,通常用于保证不被意外修改的数据。

示例一: 使用内置的 Readonly 工具类型

typescript
interface Person {
  name: string
  age: number
  slogan?: string
}

type PersonReadonly = Readonly<Person> 

// 等价于:
type PersonReadonlyManual = { 
  readonly name: string
  readonly age: number
  readonly slogan?: string
} 

示例二: 用类型体操实现一个自己的 Readonly 工具类型

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

type MyReadonly<T> = {
  readonly [P in keyof T]: T[P]
}

// 所有属性设为只读
type IKun2 = MyReadonly<IKun>

Record<Keys, Type>

定义

返回一个键值对的对象类型,该对象所有的 key 都是 Keys 类型,所有的 value 都是 Type 类型

使用场景

你可以指定多个键,并为每个键提供相同的值类型。

typescript
type Record<Keys extends string | number | symbol, Type> = { 
  [P in K]: Type
}

示例一:使用内置的 Record 工具类型

在这个例子中,Record 用来将每个城市映射到一个 Person 对象。

typescript
// 创建一个包含多个城市的记录,每个城市的属性与 Person 类型相同
type City = "上海" | "北京" | "洛杉矶"
interface Person {
  name: string
  age: number
  slogan?: string
}
type CityPersonMap = Record<City, Person>

const cityMap: CityPersonMap = {
  上海: { name: "Alice", age: 30, slogan: "Welcome to Shanghai" },
  北京: { name: "Bob", age: 25, slogan: "Welcome to Beijing" },
  洛杉矶: { name: "Charlie", age: 35, slogan: "Welcome to LA" }
}

示例二:用类型体操实现一个自己的 Record 工具类型

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

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

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

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

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

Pick<Type, Keys>

定义

返回一个从原类型中提取出某些指定属性组成的新类型。

示例一:使用内置的 Pick 工具类型

typescript
interface Person {
  name: string
  age: number
  slogan?: string
}

// 使用 Pick 提取部分属性
type NameAndAge = Pick<Person, "name" | "age"> 

// 等价于:
type NameAndAgeManual = { 
  name: string
  age: number
} 

示例二:用类型体操实现一个自己的 Pick 工具类型

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

type MyPick<T, K extends keyof T> = {
  [P in K]: T[P]
}

type IKuns = MyPick<IKun, "slogan"|"name"> 

// 等价于:
type IKuns = { 
  slogan?: string | undefined; 
  name: string; 
} 

Omit<Type, Keys>

定义

与 Pick 作用相反,返回一个从原类型中剔除掉某些指定属性后的新类型。

示例一:使用内置的 Pick 工具类型

typescript
interface Person {
  name: string
  age: number
  slogan?: string
}

// 使用 Omit 移除部分属性
type PersonWithoutSlogan = Omit<Person, "slogan"> 

// 等价于:
type PersonWithoutSloganManual = { 
  name: string
  age: number
} 

示例二:用类型体操实现一个自己的 Omit 工具类型

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

type MyOmit<T, K extends keyof T> = {
  [P in keyof T as P extends K ? never: P]: T[P]
}

type IKuns = MyOmit<IKun, "slogan"|"name">  

// 等价于:
type IKuns = { 
  age: number
} 

Exclude<UnionType, ExcludedMembers>

定义

返回一个从联合类型中剔除掉某些成员后的新类型。

使用场景

常用于从联合类型中去除不必要的成员,比如在处理枚举值时,有时需要排除掉某些无关的项。

示例一:使用内置的 Exclude 工具类型

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

// 使用 Exclude 剔除掉 rap 成员
type IKunWithoutRap = Exclude<IKun, "rap"> 

// 等价于:
type IKunWithoutRap = "sing" | "dance"

示例二:用类型体操实现一个自己的 Exclude 工具类型

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

// 分发条件类型
type MyExclude<T, E> = T extends E ? never: T 

type IKuns = MyExclude<IKun, "rap"> 

// 等价于:
type IKuns = "sing" | "dance"

有了 MyExclude,我们可以使用它来实现 MyOmit:

typescript
type MyExclude<T, U> = T extends U ? never : T
type MyOmit<T, K> = Pick<T, MyExclude<keyof T, K>>

type PropertyTypes = 'name' | 'age' | 'height'
type PropertyTypes2 = MyExclude<PropertyTypes, 'height'>

Extract<Type, Union>

定义

返回一个从原类型中提取出的某些成员组成的新类型。

使用场景

常用于提取可以与某个特定类型匹配的成员,通常用于过滤类型。

示例一:使用内置的 Extract 工具类型

typescript
type Actions = "sing" | "dance" | "rap"

// 使用 Extract 提取可以赋给 "sing" 或 "dance" 的成员
type SingOrDance = Extract<Actions, "sing" | "dance"> 

// 等价于:
type SingOrDance = "sing" | "dance"

示例二:用类型体操实现一个自己的 Extract 工具类型

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

type MyExtract<T, E> = T extends E ? T: never

type IKuns = MyExtract<IKun, "dance" | "rap"> 

// 等价于:
type IKuns = "dance" | "rap"

NonNullable<Type>

定义

返回一个从原类型中剔除掉 nullundefined 类型后的新类型。

使用场景

主要用于确保类型不包含 null 或 undefined,常见于数据校验或者确保函数参数不为 null/undefined 的场景。

示例一:使用内置的 NonNullable 工具类型

typescript
type Actions = "sing" | "dance" | "rap" | null | undefined

// 使用 NonNullable 移除 null 和 undefined
type ActionsWithoutNull = NonNullable<Actions> 

// 等价于:
type ActionsWithoutNull = "sing" | "dance" | "rap"

示例二:用类型体操实现一个自己的 NonNullable 工具类型

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

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

// type IKuns = "sing" | "dance" | "rap"
type IKuns = HYNonNullable<IKun>

ReturnType<Type>

定义

返回一个传入的函数类型的返回值类型

使用场景

常用于在处理函数类型时,自动获取其返回值的类型。

示例一:使用内置的 ReturnType 工具类型

typescript
type CalcFn = (num1: number, num2: string) => number

// 使用 ReturnType 获取函数类型的返回值类型
type CalcReturnType = ReturnType<CalcFn> // number

示例二:用类型体操实现一个自己的 ReturnType 工具类型

typescript
type CalcFnType = (num1: number, num2: string) => number
function foo() { return "abc" }

// 获取函数类型的返回值类型
type MyFnReturnType<T extends (...args: any[]) => any> = T extends (...args: any[]) => infer R ? R : never
// 获取函数类型的参数类型
type MyFnParameterType<T extends (...args: any[]) => any> = T extends (...args: infer P) => any ? P : never

type CalcReturnType = MyFnReturnType<CalcFnType>  // number
type FooReturnType = MyFnReturnType<typeof foo>   // string
type FooReturnType2 = MyFnReturnType<boolean>     // Error:类型“boolean”不满足约束“(...args: any[]) => any”
type CalcParameterType = MyFnParameterType<CalcFnType> // [num1: number, num2: string]

InstanceType<Type>

定义

返回一个传入的类类型的实例类型

使用场景

常用于从类的构造函数中获取实例类型,通常用于工厂函数或者组件类型的引用。

自定义实现:MyInstanceType

通过条件类型实现,提取构造函数类型 T 的返回类型(即实例类型 R)。

typescript
type MyInstanceType<T extends new (...args: any[]) => any> 
  = T extends new (...args: any[]) => infer R ? R: never

示例一:InstanceType VS 直接用类本身作为类型声明

typescript
class Person {}

// typeof 获取构造函数类型
type MyPerson = MyInstanceType<typeof Person>

// p1、p2 均为 Person 类型
const p1: Person = new Person()
const p2: MyPerson = new Person()

示例二:工厂函数结合 InstanceType

typescript
function factory<T extends new (...args: any[]) => any>(ctor: T): MyInstanceType<T> {
  return new ctor()
}

// 工厂函数实例化对象时,明确实例类型
const p3 = factory(Person) // 类型推导为 Person
const d = factory(Dog)     // 类型推导为 Dog

示例三:组件场景结合 InstanceType

在 Vue 组件中,ref 传入具体的组件类型时,使用 InstanceType 明确组件实例类型,用于类型推导和属性提示。

typescript
import AccountPane from './account-pane.vue'

const accountRef = ref<InstanceType<typeof AccountPane>>()
accountRef.value.xxx // 自动提示组件实例的属性和方法

动态场景中避免重复推导

直接用类本身即可明确实例类型,InstanceType 更多用于需要推导的场景,例如组件引用或工具函数。

Released under the MIT License.