在 TypeScript 中,对一个对象进行类型标注,如下:
js
interface IUser {
name: string
age?: number
}
发现,age
不是可选了,而是必选的,那怎么办呢?难道你再写一个像如下的接口吗?
js
interface IUser1 {
name: string
age: number
}
这肯定傻透了。TS 其实早就想到了,它提供了工具类型,这样我们就可以愉快的进行修改。
下图是 TS 官网 提供的工具类型,下面我将在开发中常用的工具类型一一介绍。
对象类型
在开发中,我们打交道最多的就是对象类型,因此,对象类型的工具类型占据大多数。
Partial
它接收一个对象类型,并将这个对象类型的所有属性都标记为可选,这样我们就不需要一个个将它们标记为可选属性了。
js
type User = {
name: string;
age: number;
email: string;
};
type PartialUser = Partial<User>;
const user: User = {
name: 'John Doe',
age: 30,
email: 'john.doe@example.com'
};
// 可以不实现全部的属性了!
const partialUser: PartialUser = {
name: 'John Doe',
age: 30
};
从 Partial 的使用方式我们可以看到,工具类型使用起来就像一个函数------你给它入参,它还你出参,而出入参都是类型!而这些函数预留的参数,就是类型世界中的参数:泛型。
那如果反过来,一开始我们有的就是一个内部属性均为可选的对象类型,现在需要变成必选呢?
这就要说到另一个类型编程的规律了:大部分工具类型都是成对出现的,有将属性标记为可选的 Partial,就会有将属性标记为必选的 Required,它的使用方式和 Partial 完全一致。
Required
js
type User = {
name?: string;
age?: number;
email?: string;
};
type RequiredUser = Required<User>;
const user: User = {
name: 'John Doe'
};
// 现在你必须全部实现这些属性了
const requiredUser: RequiredUser = {
name: 'John Doe',
age: 30,
email: 'john.doe@example.com'
};
Readonly
用于将属性标记为只读。
js
type User = {
name: string;
age: number;
email: string;
};
type ReadonlyUser = Readonly<User>;
const user: User = {
name: 'John Doe',
age: 30,
email: 'john.doe@example.com'
};
const readonlyUser: ReadonlyUser = {
name: 'John Doe',
age: 30,
email: 'john.doe@example.com'
};
// 修改 user 对象的属性
user.name = 'Jane Doe';
user.age = 25;
user.email = 'jane.doe@example.com';
// 修改 readonlyUser 对象的属性
// readonlyUser.name = 'Jane Doe'; // 报错
// readonlyUser.age = 25; // 报错
// readonlyUser.email = 'jane.doe@example.com'; // 报错
需要注意的是,TypeScript 内置的工具类型中,并不包括与 Readonly 结对出现的版本,你可以认为,只读通常是一个不可逆的行为,如果能够随意将只读修饰移除,就可能破坏了只读带来的安全性。
Record
我们知道,当接口描述不确定属性时,有一个可索引接口来表示:
TypeScript 中基于索引签名类型提供了一个简化版本 Record,它能够用更简洁的语法实现同样的效果:
js
type UserProps = 'name' | 'job' | 'email';
// 等价于你一个个实现这些属性了
type User = Record<UserProps, string>;
const user: User = {
name: 'John Doe',
job: 'fe-developer',
email: 'john.doe@example.com'
};
你可以使用 Record 类型来声明属性名还未确定的接口类型,如:
js
type User = Record<string, string>;
const user: User = {
name: 'John Doe',
job: 'fe-developer',
email: 'john.doe@example.com',
bio: 'Make more interesting things!',
type: 'vip',
// ...
};
Pick 与 Omit
除了对象类型的声明与属性修饰,内置工具类型中还包括用于对象类型裁剪的 Pick 与 Omit,效果类似于 Lodash 中的 Pick 与 Omit 方法。
Pick 类型接收一个对象类型,以及一个字面量类型组成的联合类型,这个联合类型只能是由对象类型的属性名组成的。它会对这个对象类型进行裁剪,只保留你传入的属性名组成的部分:
js
type User = {
name: string;
age: number;
email: string;
phone: string;
};
// 只提取其中的 name 与 age 信息
type UserBasicInfo = Pick<User, 'name' | 'age'>;
const user: User = {
name: 'John Doe',
age: 30,
email: 'john.doe@example.com',
phone: '1234567890'
};
const userBasicInfo: UserBasicInfo = {
name: 'John Doe',
age: 30
};
Omit 类型就是 Pick 类型的另一面,它的入参和 Pick 类型一致,但效果却是相反的------它会移除传入的属性名的部分,只保留剩下的部分作为新的对象类型:
js
type User = {
name: string;
age: number;
email: string;
phone: string;
};
// 只移除 phone 属性
type UserWithoutPhone = Omit<User, 'phone'>;
const user: User = {
name: 'John Doe',
age: 30,
email: 'john.doe@example.com',
phone: '1234567890'
};
const userWithoutPhone: UserWithoutPhone = {
name: 'John Doe',
age: 30,
email: 'john.doe@example.com'
};
Pick 与 Omit 类型是类型编程中相当重要的一个部分,举例来说,我们可以先声明一个代表全局所有状态的大型接口类型:
js
type User = {
name: string;
age: number;
email: string;
phone: string;
address: string;
gender: string;
occupation: string;
education: string;
hobby: string;
bio: string;
};
type UserBasicInfo = Pick<User, 'name' | 'age' | 'email'>;
const userBasicInfo: UserBasicInfo = {
name: 'John Doe',
age: 30,
email: 'john.doe@example.com'
};
type UserDetailedInfo = Omit<User, 'name' | 'age' | 'email'>;
const userDetailedInfo: UserDetailedInfo = {
phone: '1234567890',
address: '123 Main St',
gender: 'male',
occupation: 'developer',
education: 'Bachelor',
hobby: 'reading',
bio: 'A passionate developer'
};
集合类型
集合类型主要有两个工具类型: Exclude
和 Extract
,这两个名字可能不太好理解,但如果我说 差集 与 交集 你就懂了。
Exclude
它能够从一个类型中移除另一个类型中也存在的部分:
js
type UserProps = 'name' | 'age' | 'email' | 'phone' | 'address';
type RequiredUserProps = 'name' | 'email';
// OptionalUserProps = UserProps - RequiredUserProps
type OptionalUserProps = Exclude<UserProps, RequiredUserProps>;
const optionalUserProps: OptionalUserProps = 'age' | 'phone' | 'address';
Extract
用于提取另一个类型中也存在的部分,即交集。
js
type UserProps = 'name' | 'age' | 'email' | 'phone' | 'address';
type RequiredUserProps = 'name' | 'email';
type RequiredUserPropsOnly = Extract<UserProps, RequiredUserProps>;
const requiredUserPropsOnly: RequiredUserPropsOnly = 'name' | 'email';
函数类型
不妨先想想,对于函数类型,工具类型能起到什么作用?
函数类型=参数类型+返回值类型,这个定律适用于所有的函数类型定义。而我们一般又不会去修改参数与返回值位置的类型,那就只剩下读取了。
内置工具类型中提供了 Parameters
和 ReturnType
这两个类型来提取函数的参数类型与返回值类型。
js
type Add = (x: number, y: number) => number;
type AddParams = Parameters<Add>; // [number, number] 类型
type AddResult = ReturnType<Add>; // number 类型
const addParams: AddParams = [1, 2];
const addResult: AddResult = 3;
那么如果,我们只有一个函数,而并没有这个函数类型呢?
此时可以使用 TypeScript 提供的类型查询操作符,即 typeof(记得和 JavaScript 的 typeof 区分一下),来获得一个函数的结构化类型,再配合工具类型即可即可:
js
const addHandler = (x: number, y: number) => x + y;
type Add = typeof addHandler; // (x: number, y: number) => number;
type AddParams = Parameters<Add>; // [number, number] 类型
type AddResult = ReturnType<Add>; // number 类型
const addParams: AddParams = [1, 2];
const addResult: AddResult = 3;
你可能会想到,对于异步函数类型,提取出的返回值类型是一个 Promise<string>
这样的类型,如果我想提取 Promise 内部的 string 类型呢?贴心的 TypeScript 为你准备了 Awaited 类型用于解决这样的问题:
js
// 定义一个函数,该函数返回一个 Promise 对象
async function getPromise() {
return new Promise<string>((resolve) => {
setTimeout(() => {
resolve("Hello, World!");
}, 1000);
});
}
type Result = Awaited<ReturnType<typeof getPromise>>; // string 类型
以上这些虽然不是全部的 TypeScript 内置工具类型,但一定是最常用的部分。熟练使用这些内置工具类型,在开发中一定会如鱼得水。