一. 泛型语法的基本使用
前言
在软件开发中,我们往往希望代码能够重用、灵活,这样不仅使得代码更易于维护,还能适应更多的场景。 我们可以通过函数来封装代码逻辑,并通过传递不同的参数,让同一个函数实现不同的功能。 然而,对于参数的类型能不能也具备灵活性呢?
认识泛型(Generics)
引言
泛型正是解决这种需求的工具。泛型让我们可以在函数、类或接口中使用类型参数,从而增强代码的重用性和类型安全性。
什么是类型的参数化?
举个简单的需求:我们希望实现一个函数,这个函数接收一个参数并返回这个参数本身。为了确保类型一致,我们希望参数类型和返回类型一致,并且这个类型是可以变化的。
function foo(arg: number): number {
return arg
}
const res1 = foo(10) // 返回值是 number 类型
这个代码可以运行,但只能用于 number 类型,换成其他类型(如 string 或 boolean)就会报错。例如:
const res2 = foo('hello') // Error:类型 “string” 不能赋给类型 “number”
为什么需要泛型?
如果要适应不同的类型,是否可以考虑使用联合类型或 any 类型:
① 使用联合类型(number | string)虽然可以接收不同的类型,但会丢失精确的类型信息,如下所示:
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 类型:
function foo(arg: any): any {
return arg
}
const res1 = foo(10) // res1 是 any 类型
const res2 = foo('hello') // res2 也是 any 类型
泛型实现类型参数化
引言
泛型可以解决上述问题。使用泛型时,我们通过一种特殊的类型变量(Type Variable)来捕获参数的类型信息,并使返回值和参数保持一致。这个类型变量可以在调用时动态地确定。
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
推导出来的是通用类型。
泛型的多参数和常用参数名称
引言
泛型的强大之处在于它允许我们在函数、接口和类中定义多个类型参数,满足不同需求。
例如:
function pair<T, U>(first: T, second: U): [T, U] {
return [first, second]
}
const result = pair<string, number>('hello', 42) // 返回值 result 的类型是 [string, number]
在日常开发中,通常使用以下字母来表示不同的类型参数:
/**
T: 表示通用类型(Type)
K、V:表示键值对中的键(Key)和值(Value)
E: 表示元素类型(Element)
O: 表示对象类型(Object)
*/
二. 泛型在接口与类中的使用
前言
在 TypeScript 中,泛型不仅可以在函数中使用,也可以用于接口和类。通过泛型,接口和类可以接收不同的类型参数,这样它们可以灵活地适配多种类型。
泛型接口
引言
在接口中使用泛型可以让接口定义更灵活。
示例:
定义一个接口 IKun,允许类型参数用于其中的属性和方法。
// 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 等任意类型来表示。
// 使用泛型 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 类型。
使用泛型类时,不仅可以存储数值坐标,还可以表示字符串坐标、布尔值等。
更多练习:泛型的应用
下面再给出几个典型的泛型接口和类的应用案例,帮助深入理解。
- 泛型接口示例:数据包装器
创建一个数据包装接口,包含一个泛型类型 T 的数据和一个 status 字段:
typescriptinterface 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' }
- 泛型类示例:简单的存储容器
定义一个泛型类 Container,用于存储不同类型的数据,并提供一个 get 方法返回数据。
typescriptclass 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 属性。
示例:
// 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,用于获取对象中指定的属性值,但同时确保属性是对象实际包含的。
// 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 的关键字,用来获取某个类型的所有属性名组成的字符串字面量联合类型。
示例一:提取对象的键``
type Person = {
name: string
age: number
isActive: boolean
}
type PersonKeys = keyof Person // 'name' | 'age' | 'isActive'
在这个例子中,keyof Person 提取出 Person 类型的所有键,PersonKeys 的类型是一个联合类型,即 'name' | 'age' | 'isActive'。
示例二:数字键的情况
type MyObject = {
0: string
1: number
}
type MyObjectKeys = keyof MyObject // '0' | '1'
这里 keyof MyObject 会返回 '0' | '1',即使对象的键是数字类型,keyof 返回的结果仍然是字符串字面量类型。
示例三:keyof any
在 TypeScript 中,
keyof any
是一个非常特殊的类型,它表示所有 JavaScript 对象有效的 key 类型组成的联合类型。
type Keys = keyof any // => number | string | symbol
为什么是 number | string | symbol?
- string:所有字符串类型的键,例如 "name"、"age"。
- number:数字类型的键,实际上 JavaScript 对象中的数字键会被自动转换成字符串(例如,{ 1: "one" },其实是 {"1": "one"})。
- symbol:符号类型的键,用于唯一标识对象属性。
所以,keyof any 可以表示任何类型的对象的键,不限于特定类型的对象。
举个例子:
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 关键字:
type MapType<T> = {
[P in keyof T]: T[P]
}
- keyof:基于对象类型 T 的所有属性名生成一个联合类型。
- P in keyof T:使用 in 遍历联合类型中的每个属性。
- T[P]:通过索引 P 访问对象 T 的属性值。
示例:
假设我们有一个表示 IPerson 的接口:
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 等价于:
type NewPerson = {
name: string
age: number
}
映射类型提供了一种灵活的方式来创建新类型,让我们可以在不复制代码的情况下对已有类型进行结构性变更。它为类型的复用和扩展带来了很大的便利。
⚠️ 注意
映射类型不支持使用 interface 来定义
映射修饰符(Mapping Modifiers)
在映射类型中,我们可以使用一些修饰符来控制属性的特性,比如将属性设置为只读(readonly)或可选(?)。
常用映射修饰符
- readonly:设置属性为只读,使属性不可修改。
- ?(可选):设置属性为可选,使属性在对象中可以省略。
修改符的前缀
- 前缀 +:为属性添加修饰符(如 +readonly 表示将属性设为只读)。
- 前缀 -:移除属性的修饰符(如 -readonly 表示移除只读修饰符)。
⚠️ 注意
不写前缀时,默认为 +。
示例:
假设我们希望将 IPerson 的所有属性都设置为可选和非只读:
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. 全部属性只读
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)
什么是条件类型?
条件类型允许你根据某个类型是否满足特定条件来选择不同的类型。它的语法类似于条件表达式:
SomeType extends OtherType ? TrueType : FalseType
- SomeType extends OtherType:检查 SomeType 是否可以赋值给 OtherType。
- TrueType:如果条件为真,选择的类型。
- FalseType:如果条件为假,选择的类型。
示例:
假设我们想创建一个函数,根据输入参数的类型返回不同的类型:
// 定义条件类型的泛型函数签名
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 提供的一个关键字,用于在条件类型中推断类型变量。它允许你从一个类型中提取出部分类型,并在条件类型的结果中使用这些推断出的类型。
示例:提取函数的返回类型
假设我们想创建一个类型工具,用于提取函数的返回类型:
// 定义一个函数类型
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。
示例:提取函数的参数类型
同样,我们可以创建一个类型工具来提取函数的参数类型:
// 定义一个函数类型
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)
什么是分发条件类型?
当在泛型中使用条件类型时,如果传入的是一个联合类型,条件类型会分发到联合类型的每个成员上。
这意味着条件类型会针对联合类型中的每个成员单独应用,最终结果是各个应用结果的联合。
示例:
// 定义一个条件类型,将类型转换为数组
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[]
解析:
- 传入联合类型:number | string
- 条件类型分发:
typescriptToArray<number | string> // 等价于: number extends any ? number[] : never => number[] string extends any ? string[] : never => string[]
- 结果: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
创建一个类型工具,用于判断一个类型是否是数组,并提取数组元素类型:
// 判断类型是否为数组,如果是,提取元素类型
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。
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 工具类型
interface Person {
name: string
age: number
slogan?: string
}
type PersonOptional = Partial<Person>
// 等价于:
type PersonOptionalManual = {
name?: string
age?: number
slogan?: string
}
示例二: 用类型体操实现一个自己的 Partial 工具类型
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 工具类型
interface Person {
name: string
age: number
slogan?: string
}
type PersonRequired = Required<Person>
// 等价于:
type PersonRequiredManual = {
name: string
age: number
slogan: string
}
示例二: 用类型体操实现一个自己的 Required 工具类型
interface IKun {
name: string
age: number
slogan?: string
}
type MyRequired<T> = {
[P in keyof T]-?: T[P]
}
// 所有属性设为必填
type IKunRequired = MyRequired<IKun>
Readonly<Type>
定义
返回一个与原类型相同,属性都是只读的新类型。
使用场景
常用于确保对象一旦创建后不可修改,通常用于保证不被意外修改的数据。
示例一: 使用内置的 Readonly 工具类型
interface Person {
name: string
age: number
slogan?: string
}
type PersonReadonly = Readonly<Person>
// 等价于:
type PersonReadonlyManual = {
readonly name: string
readonly age: number
readonly slogan?: string
}
示例二: 用类型体操实现一个自己的 Readonly 工具类型
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 类型。
使用场景
你可以指定多个键,并为每个键提供相同的值类型。
type Record<Keys extends string | number | symbol, Type> = {
[P in K]: Type
}
示例一:使用内置的 Record 工具类型
在这个例子中,Record 用来将每个城市映射到一个 Person 对象。
// 创建一个包含多个城市的记录,每个城市的属性与 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 工具类型
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 工具类型
interface Person {
name: string
age: number
slogan?: string
}
// 使用 Pick 提取部分属性
type NameAndAge = Pick<Person, "name" | "age">
// 等价于:
type NameAndAgeManual = {
name: string
age: number
}
示例二:用类型体操实现一个自己的 Pick 工具类型
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 工具类型
interface Person {
name: string
age: number
slogan?: string
}
// 使用 Omit 移除部分属性
type PersonWithoutSlogan = Omit<Person, "slogan">
// 等价于:
type PersonWithoutSloganManual = {
name: string
age: number
}
示例二:用类型体操实现一个自己的 Omit 工具类型
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 工具类型
type IKun = "sing" | "dance" | "rap"
// 使用 Exclude 剔除掉 rap 成员
type IKunWithoutRap = Exclude<IKun, "rap">
// 等价于:
type IKunWithoutRap = "sing" | "dance"
示例二:用类型体操实现一个自己的 Exclude 工具类型
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:
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 工具类型
type Actions = "sing" | "dance" | "rap"
// 使用 Extract 提取可以赋给 "sing" 或 "dance" 的成员
type SingOrDance = Extract<Actions, "sing" | "dance">
// 等价于:
type SingOrDance = "sing" | "dance"
示例二:用类型体操实现一个自己的 Extract 工具类型
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>
定义
返回一个从原类型中剔除掉
null
和undefined
类型后的新类型。
使用场景
主要用于确保类型不包含 null 或 undefined,常见于数据校验或者确保函数参数不为 null/undefined 的场景。
示例一:使用内置的 NonNullable 工具类型
type Actions = "sing" | "dance" | "rap" | null | undefined
// 使用 NonNullable 移除 null 和 undefined
type ActionsWithoutNull = NonNullable<Actions>
// 等价于:
type ActionsWithoutNull = "sing" | "dance" | "rap"
示例二:用类型体操实现一个自己的 NonNullable 工具类型
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 工具类型
type CalcFn = (num1: number, num2: string) => number
// 使用 ReturnType 获取函数类型的返回值类型
type CalcReturnType = ReturnType<CalcFn> // number
示例二:用类型体操实现一个自己的 ReturnType 工具类型
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)。
type MyInstanceType<T extends new (...args: any[]) => any>
= T extends new (...args: any[]) => infer R ? R: never
示例一:InstanceType VS 直接用类本身作为类型声明
class Person {}
// typeof 获取构造函数类型
type MyPerson = MyInstanceType<typeof Person>
// p1、p2 均为 Person 类型
const p1: Person = new Person()
const p2: MyPerson = new Person()
示例二:工厂函数结合 InstanceType
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 明确组件实例类型,用于类型推导和属性提示。
import AccountPane from './account-pane.vue'
const accountRef = ref<InstanceType<typeof AccountPane>>()
accountRef.value.xxx // 自动提示组件实例的属性和方法
动态场景中避免重复推导
直接用类本身即可明确实例类型,InstanceType 更多用于需要推导的场景,例如组件引用或工具函数。