八、泛型
泛型是静态类型语言的基本特征,允许将类型作为参数传递给另一个类型、函数、或者其他结构。
泛型作为将类型安全引入组件的一种方式。这些组件接受参数和返回值,其类型将是不确定的,直到它在代码中被使用。
function pickObjectKeys<T, K extends keyof T>(obj: T, keys: K[]) {
let result = {} as Pick<T, K>
for (const key of keys) {
if (key in obj) {
result[key] = obj[key]
}
}
return result
}
const language = {
name: "TypeScript",
age: 8,
extensions: ['ts', 'tsx']
}
const ageAndExtensions = pickObjectKeys(language, ['age', 'extensions'])
<T, K extends keyof T> 为函数声明了两个参数类型,其中 K 被分配给了一个类型,该类型是 T 中的 key 的集合。然后将 obj 参数设置为 T,表示任何类型,并将 keys 设置为数组,无论 K 是什么类型。
1、在函数中使用泛型
分配泛型参数
function identity<T>(value: T): T {
return value;
}
函数转化为接受泛型类型参数 T 的泛型函数,它第一个参数的类型,然后将返回类型也设置为 T
const result = identity(123);
// result 的类型为 123,是传入的数字:
// function identity<123>(value: 123): 123
显式地将泛型类型参数设置为想要的类型
const result = identity<number>(123);
// result的类型就是 number:
// function identity<number>(value: number): number
直接传递类型参数
async function fetchApi<ResultType>(path: string): Promise<ResultType> {
const response = await fetch(`https://example.com/api${path}`);
return response.json();
}
这个函数是异步的,因此会返回一个 Promise 对象。 TypeScript 中的 Promise 类型本身是一个泛型类型,它接受 Promise 解析为的值的类型。
type User = {
name: string;
}
const data = await fetchApi<User[]>('/users')
创建了一个名为 User 的新类型,并使用该类型的数组 (User[]) 作为 ResultType 泛型参数的类型。data 变量现在的类型是 User[] 而不是 any。
当使用 await 异步处理函数的结果时,返回类型将是
Promise<T>中的 T 类型,在这个示例中就是泛型类型ResultType。
默认类型参数
fetchApi函数,如果调用代码不包含泛型类型参数,则 ResultType 将推断为 unknow。
const data = await fetchApi('/users')
console.log(data.a)
尝试访问data的a属性,但是由于data是unknow类型,将无法访问对象的属性。
不打算为泛型函数的每次调用添加特定的类型,则可以为泛型类型参数添加默认类型。通过在泛型类型参数后面添加 = DefaultType 来完成:
async function fetchApi<ResultType = Record<string, any>>(path: string): Promise<ResultType> {
const response = await fetch(`https://example.com/api${path}`);
return response.json();
}
const data = await fetchApi('/users')
console.log(data.a)
默认类型 Record<string, any>。 将data识别为具有string类型的键和any类型值的对象,从而允许访问其属性。
类型参数约束
🌰:只能存储所有属性值都为字符串类型的对象
创建一个函数,该函数接受任何对象并返回另一个对象,其 key 值与原始对象相同,但所有值都转换为字符串
function stringifyObjectKeyValues<T extends Record<string, any>>(obj: T) {
return Object.keys(obj).reduce((acc, key) => ({
...acc,
[key]: JSON.stringify(obj[key])
}), {} as { [K in keyof T]: string })
}
- 函数使用
reduce数组方法遍历包含原始对象的key的数组,将属性值字符串化并将它们添加到新数组中。 extends Record<string, any>被称为泛型类型约束,它允许指定泛型类型必须可分配给extends关键字之后的类型。 在这种情况下,Record<string, any>表示具有string类型的键和any类型的值的对象。reducer函数的返回类型是基于累加器的初始值。{} as { [K in keyof T]: string }通过对空对象{}使用类型断言将累加器的初始值的类型设置为{ [K in keyof T]: string }。type { [K in keyof T]: string }创建了一个新类型,其键与 T 相同,但所有值都设置为字符串类型,这称为映射类型。
使用:
const stringifiedValues = stringifyObjectKeyValues({ a: "1", b: 2, c: true, d: [1, 2, 3]})
2、接口,类和类型中使用泛型
一个类可能具有不同类型的属性,具体取决于传入构造函数的内容
接口和类中的泛型
class HttpApplication<Context> {
context: Context
constructor(context: Context) {
this.context = context;
}
// ...
get(url: string, handler: (context: Context) => Promise<void>): this {
// ...
return this;
}
}
const context = { someValue: true };
const app = new HttpApplication(context);
app.get('/api', async () => {
console.log(context.someValue)
});
这个类储存了一个 context,它的类型作为 get 方法中handler函数的参数类型传入。 在使用时,传递给 get 方法的handler的参数类型将从传递给类构造函数的内容中推断出来
???
自定义类型中的泛型
泛型类型通常用于创建工具类型,尤其是在使用映射类型
Partial实用工具类型,它传入类型 T 并返回另一种具有与 T 相同的类型,但它们的所有字段都设置为可选
type Partial<T> = {
[P in keyof T]?: T[P];
};
Partial 接受一个类型,遍历它的属性类型,然后将它们作为可选的新类型返回。
3、使用泛型创建映射类型
需要创建一个与另一种类型具有相同结构的类型。这意味着它们应该具有相同的属性,但属性的类型不同
type BooleanFields<T> = {
[K in keyof T]: boolean;
}
使用 [K in keyof T] 指定新类型将具有的属性。keyof T 用于返回 T 中所有可用属性的名称。然后使用 K in 来指定新类型的属性是keyof T返回的类型中可用的所有属性
🌰:假设有一个数据库模型User。 从数据库中获取此模型的记录时,还可以传递一个指定要返回哪些字段的对象。 该对象将具有与模型相同的属性,但类型设置为布尔值。
type User = {
email: string;
name: string;
}
type UserFetchOptions = BooleanFields<User>;
// UserFetchOptions的类型:
// type UserFetchOptions = {
// email: boolean;
// name: boolean;
// }
4、条件类型
基础条件类型
条件类型是泛型类型,根据某些条件有不同的结果类型
type IsStringType<T> = T extends string ? true : false;
type A = 'abc'
type B = {
name: string
}
type ResultA = IsStringType<A>;
type ResultB = IsStringType<B>;
这里 ResultA 类型设置为 true,而 ResultB 类型设置为 false。
条件类型的一个有用特性是它允许使用特殊关键字 infer 在 extends 中推断类型信息。 可以在条件为真的分支中使用这种新类型。 此功能的一种用途是检索任何函数类型的返回类型。
type GetReturnType<T> = T extends (...args: any[]) => infer U ? U : never;
在类型声明本身内部,检查类型 T 是否扩展了与接受可变数量参数(包括0)的函数签名匹配的类型,然后推断该返回函数的类型,创建一个新类型 U,它可用于条件的真实分支。 U 的类型将绑定到传递函数的返回值的类型。 如果传递的类型 T 不是函数,则代码将返回类型nerver。
用法
function someFun() {
return true
}
type ReturnTypeOfSomeFun = GetReturnType<typeof someFun>;
由于 someFun 变量的类型是函数,因此条件类型将计算条件为真的分支。这将返回类型 U 作为结果。 U类型是根据函数的返回类型推断出来的,在本例中是boolean。 如果检查 ReturnTypeOfSomeFun 的类型,会发现它被设置为了boolean类型。
高级条件类型
创建一个名为 NestedOmit<T, KeysToOmit> 的类型,它可以省略对象中的字段,就像现有的Omit<T, KeysToOmit> 实用程序类型一样,但也允许使用点表示法省略嵌套字段。
一些解释
-
Omit<T, KeysToOmit>是 TypeScript 中的一个工具类型,用于从现有类型T中排除指定的属性(KeysToOmit),从而构造一个新的类型。它通常用于创建一个不包含某些键的类型子集。 -
T extends Record<string, any>是 TypeScript 中的一种类型约束,表示泛型类型T必须是一个对象类型,且其键为string类型,值可以是任意类型(any)。这种约束通常用于确保泛型参数是一个对象。 -
${infer KeyPart1}.${infer KeyPart2}是一种模板文本类型的模式匹配语法(通常用于从字符串类型中提取嵌套对象的键路径)infer KeyPart1:推断.之前的部分,并将其赋值给KeyPart1。infer KeyPart2:推断.之后的部分,并将其赋值给KeyPart2。.:匹配字符串中的点号(.)
type NestedOmit<T extends Record<string, any>, KeysToOmit extends string> =
KeysToOmit extends `${infer KeyPart1}.${infer KeyPart2}`
?
KeyPart1 extends keyof T
?
Omit<T, KeyPart1>
& {
[NewKeys in KeyPart1]: NestedOmit<T[NewKeys], KeyPart2>
}
: T
: Omit<T, KeysToOmit>;
解释代码:
[NewKeys in KeyPart1]:NestedOmit<T[NewKeys], KeyPart2>是一个映射类型,其中属性是可分配给KeyPart1的属性,也就是上面从KeysToOmit中提取的部分。这是需要删除的字段的父级。 如果传入了a.b.c,那么在第一次它将是a中的NewKeys。 然后,将此属性的类型设置为递归调用NestedOmit实用程序类型的结果,但现在使用T[NewKeys]作为第一个类型参数传递 T 内的此属性的类型,并作为第二个类型参数传递点符号的其余key,在KeyPart2中可用。- 如果条件
KeysToOmit extends ${infer KeyPart1}.${infer KeyPart2}为false,则表示KeysToOmit未使用点表示法,因此可以使用Omit实用程序类型。
type NestedObject = {
a: {
b: {
c: number;
d: number;
};
e: number;
};
f: number;
};
type Result = NestedOmit<NestedObject, "a.b.c">;
/**
Result 的类型是 = {
a: {
b: {
c: number;
d: number;
};
e: number;
};
f: number;
};
*/
// 移除了a.b.c
九、装饰器
1、概念
装饰器就是可以添加到类及其成员的函数(Decorator)
type Decorator = (target: Input, context: {
kind: string;
name: string | symbol;
access: {
get?(): unknown;
set?(value: unknown): void;
},
private?: boolean;
static?: boolean;
addInitializer?(initializer: () => void): void;
}) => Output | void;
-
target: 要装饰的元素,类型是Input -
context:包含有关如何声明修饰方法的元数据kind:装饰值的类型name:被装饰对象名称access:引用getter和setter方法来访问装饰对象的对象private:被装饰的对象是否是私有类成员static:被装饰的对象是否是静态类成员addInitialier:在构造函数开头(或定义类)添加自定义初始化逻辑的方法
-
Output:表示Decorator函数返回值的类型
2、装饰器类型
类装饰器
type ClassDecorator = (value: Function, context: {
kind: 'class'
name: stirng | undefined
addInitializer(initializer: () => void): void
}) => Function | void
使用装饰器向Rocket类添加两个属性:fuel和isEmpty()
function WithFuel(target: typeof Rocket, context): typeof Rocket {
if(context.kind === 'class') {
return class extends target {
fuel: number = 50
isEmpty(): boolean {
return this.fuel == 0
}
}
}
}
// 使用
@WithFuel
class Rocket {
fuel: number = 33
}
const rocket = new Rocket()
console.log((rocket as any).fuel) // 50
console.log(`empty? ${(rocket as any).isEmpty()}`) // empty? false
将rocket转换为any类型才能访问新的属性。这是因为装饰器无法影响类型的结构。
如果Rocket有一个具有不同值的fuel属性,WithFuel装饰器将会覆盖该值:
方法装饰器
type ClassMethodDecorator = (target: Function, context: {
kind: 'method',
name: string | symbol
access: {get(): unknow}
static: boolean
private: boolean
addInitializer(initialier: () => void): void
}) => Function | void
使用方法装饰器将一个方法标记为已废弃,记录一条消息来警告用户,并告知他们应该使用哪个方法代替:
function deprecatedMethod(target: Function, context) {
if(context.kind === 'method') {
return function (...args: any[]) {
console.log(`${context.name} is deprecated`)
return target.apply(this, args)
}
}
}
@WithFuel
class Rocket {
fuel: number = 75
@deprecatedMethod
isReadyForLanuch(): Boolaen {
return !(this as any).isEmpty()
}
}
const rocket = new Rocket();
console.log(`Is ready for launch? ${rocket.isReadyForLaunch()}`)
// isReadyForLaunch is deprecated
// Is the ready for launch? true
isReadyForLaunch()方法中,引用了通过WithFuel装饰器添加的isEmpty方法。注意,必须将其转换为any类型的实例,与之前一样。当调用isReadyForLaunch()方法,显示警告消息被正确地打印出来
属性装饰器
属性装饰器与方法装饰器的类型非常相似
type ClassPropertyDecorator = (target: undefined, context: {
kind: "field"
name: string | symbol
access: { get(): unknown, set(value: unknown): void }
static: boolean
private: boolean
}) => (initialValue: unknown) => unknown | void
function deprecatedProperty(_: any, context) {
if (context.kind === "field") {
return function (initialValue: any) {
console.log(`${context.name} is deprecated.`)
return initialValue
}
}
}
访问装饰器
与方法装饰器非常类型,是针对getter和setter的装饰器
type ClassSetterDecorator = (target: Function, context: {
kind: "setter"
name: string | symbol
access: { set(value: unknown): void }
static: boolean
private: boolean
addInitializer(initializer: () => void): void
}) => Function | void
type ClassGetterDecorator = (value: Function, context: {
kind: "getter"
name: string | symbol
access: { get(): unknown }
static: boolean
private: boolean
addInitializer(initializer: () => void): void
}) => Function | void
可以将 deprecatedMethod 和 deprecatedProperty 修饰合并到一个已弃用的函数中,该函数也支持 getter 和 setter
function deprecated(target, context) {
const kind = context.kind
const msg = `${context.name} is deprecated`
if (kind === "method" || kind === "getter" || kind === "setter") {
return function (...args: any[]) {
console.log(msg)
return target.apply(this, args)
}
} else if (kind === "field") {
return function (initialValue: any) {
console.log(msg)
return initialValue
}
}
}
3、使用
计算执行时间
想要估计运行一个函数需要多长时间,以此来衡量应用的性能
class Rocket {
@measure
launch() {
console.log("3,2,1...")
}
}
import { performance } from "perf_hooks"
function measure(target: Function, conetxt) {
if(context.kind === 'method') {
return function (...args: any[]) {
const start = performance.now()
const result = target.apply(this, args)
const end = performance.now()
console.log(`Time: ${end - start} s`)
return result
}
}
}
计算执行时间,可以使用 Node.js 标准库中的性能钩子(Performance Hooks)API
使用装饰器工厂函数
将装饰器配置为在特定场景中采取不同的行为。装饰器工厂是返回装饰器的函数,这样就能够通过在工厂中传递一些参数来自定义装饰器的行为。
function fill(value: number) {
return function(_, context) {
if (context.kind === "field") {
return function (initialValue: number) {
return value + initialValue
}
}
}
}
class Rocket {
@fill(20)
fuel: number = 50
}
const rocket = new Rocket()
console.log(rocket.fuel) // 70
自动错误拦截
装饰器的另一个常见用例是检查方法调用的前置条件和后置条件
🌰:假设要在调用 launch() 方法之前确保 Fuel 至少为给定值:
假设有一个 Rocket 类,它有一个 launchToMars 方法。 要发射火箭,燃料(fuel)必须高于一个值,例如 75。
function minimumFuel(fuel: number) {
return function(target: Function, context) {
if (context.kind === "method") {
return function (...args: any[]) {
if (this.fuel > fuel) {
return target.apply(this, args)
} else {
console.log(`Not enough fuel. Required: ${fuel}, got ${this.fuel}`)
}
}
}
}
}
class Rocket {
fuel = 50
@minimumFuel(75)
launch() {
console.log("3... 2... 1... 🚀")
}
}
const rocket = new Rocket()
rocket.launch()
// Not enough fuel. Required: 75, got 50
十、工具类型
1、Partial
将传入的属性变为可选项,适用于对类型结构不明确的情况
适用两个关键字keyof和in
- keyof:用来取得接口所有的key值
- in:遍历枚举类型
keyof可以产生联合类型,in可以遍历枚举类型。一起使用Partial定义
// 将T中所有的属性设置为可选
type Partial<T> = {
[P in keyof T]?: T[P]
}
keyof T 用来获取 T 所有属性名, 然后使用 in 进行遍历, 将值赋给 P, 最后 T[P] 取得相应属性的值。中间的?就用来将属性设置为可选。
🌰:
type Person = {
name: string;
age: number;
height: number;
}
type PartialPerson = Partial<Person>;
// PartialPerson 的类型为 {name?: string; age?: number; height?: number;}
const person: PartialPerson = {
name: "zhangsan";
}
2、Required
将传入的属性变为必选项
/**
* Make all properties in T required
* 将T中的所有属性设置为必选
*/
type Required<T> = {
[P in keyof T]-?: T[P];
};
这里使用-?将属性设置为必选,可以理解为减去问号。使用形式和上面的Partial差不多
3、Readonly
将T类型的所有属性设置为只读(readonly),构造出来类型的属性不能被再次赋值
/**
* Make all properties in T readonly
*/
type Readonly<T> = {
readonly [P in keyof T]: T[P];
};
type Person = {
name: string;
age: number;
}
type ReadonlyPerson = Readonly<Person>;
const person: ReadonlyPerson = {
name: "zhangsan",
age: 18
}
person.age = 20; // Error: cannot reassign a readonly property
通过 Readonly 将Person的属性转化成了只读,不能再进行赋值操作。Readonly 类型对于冻结对象非常有用。
4、Pick<Type, Keys>
从 Type 类型中挑选部分属性 Keys 来构造新的类型。
type Pick<T, K extends keyof T> = {
[P in K]: T[P];
};
type Person = {
name: string;
age: number;
height: number;
}
const person: Pick<Person, "name" | "age"> = {
name: "zhangsan",
age: 18
}
5、Record<Keys, Type>
用来构造一个类型,其属性名的类型为Keys中的类型,属性值的类型为Type。这个工具类型可用来将某个类型的属性映射到另一个类型上
type Record<K extends keyof any, T> = {
[P in K]: T;
};
type Pageinfo = {
title: string;
}
type Page = 'home' | 'about' | 'contact';
const page: Record<Page, Pageinfo> = {
about: {title: 'about'},
contact: {title: 'contact'},
home: {title: 'home'},
}
6、Exclude<Type, ExcludedUnion>
用于从类型type中去除不在Excluded Union类型中的成员
type Exclude<T, U> = T extends U ? never : T;
type Person = {
name: string;
age: number;
height: number;
}
const person: Exclude<Person, "age" | "sex"> = {
name: "zhangsan";
height: 180;
}
将Person类型中的age属性给剔除了,只会剔除两个参数中都包含的属性
7、Extract<Type, Union>
从类型Type中取出可分配给Union类型的成员
type Extract<T, U> = T extends U ? T : never;
type ExtractedType = Extract<"x" | "y" | "z", "x" | "y">;
// "x" | "y"
用于找出两种类型的公共部分
interface Human {
id: string;
name: string;
surname: string;
}
interface Cat {
id: string;
name: string;
sound: string;
}
// "id" | "name"
type CommonKeys = Extract<keyof Human, keyof Cat>;
8、Omit<Type, Keys>
从一个对象中剔除若干个属性,得到需要的新类型
type Omit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>;
type Person = {
name: string;
age: number;
height: number;
}
const person: Omit<Person, "age" | "height"> = {
name: "zhangsan";
}
从Person类型中剔除了 age 和 height 属性,只剩下 name 属性
9、ReturnType
返回函数返回值的类型
type ReturnType<T extends (...args: any) => any> = T extends (...args: any) => infer R ? R : any;
function foo(type): boolean {
return type === 0
}
type FooType = ReturnType<typeof foo>
这里使用 typeof 是为了获取 foo 的函数签名,等价于 (type: any) => boolean
10、InstanceType
返回 Type 构造函数类型的实例类型
type InstanceType<T extends abstract new (...args: any) => any> = T extends abstract new (...args: any) => infer R ? R : any;
class Person {
name: string;
age: number;
constructor(person: { name: string; age: number }) {
this.name = person.name;
this.age = person.age;
}
}
type PersonInstanceType = InstanceType<typeof Person>;
// PersonInstanceType 的类型:{ name: string; age: number }
当我们在 TypeScript 中创建动态类时,InstanceType可以用于检索动态实例的类型
11、Parameters
从函数类型Type的参数中使用的类型构造一个元组类型
type Parameters<T extends (...args: any) => any> = T extends (...args: infer P) => any ? P : never;
const add = (x: number, y: number) => {
return x + y;
};
type FunctionParameters = Parameters<typeof add>;
// FunctionParameters 的类型:[x: number, y: number]
对于获取函数参数的类型以确保类型安全
const saveUser = (user: { name: string; height: number; age: number }) => {
// ...
};
const user: Parameters<typeof saveUser>[0] = {
name: "zhangsan",
height: 180,
age: 18,
};
12、ConstructorParameter
从构造函数的类型来构造元组或数组类型
type ConstructorParameters<T extends abstract new (...args: any) => any> = T extends abstract new (...args: infer P) => any ? P : never;
它类似于参数,但适用于类构造函数:
class Person {
private name: string;
private age: number;
constructor(person: { name: string; age: number }) {
this.name = person.name;
this.age = person.age;
}
}
const params: ConstructorParameters<typeof Person>[0] = {
name: "zhangsan",
age: 18,
};
当使用外部库时,它有助于确保构造函数接受我们传入的参数
13、NonNullable
通过从Type中排除null和undefined来创建新类型。它就等价于Exclude<T, null | undefined>
type NonNullable<T> = T extends null | undefined ? never : T;
type Type = string | null | undefined;
// string
type NonNullableType = NonNullable<Type>;