原文链接:TypeScript Types as a Programming Language
你知道吗?TypeScript 是图灵完备的。在本文中,我将把类型定义当作编写程序来对待。我的目标并非用 TypeScript 编写Doom游戏或执行数学运算 —— 这些早就有人实现过了:
尽管这些案例令人惊叹,但也引出了一个问题:我们为什么要这么做?答案是:通过将类型当作程序来编写,从而更熟练地掌握类型定义技巧。
泛型类型:类型世界的 “函数”
在 TypeScript 中,你可以定义依赖于其他类型的类型。这类类型的工作方式类似函数:接收一个输入类型,产出一个输出类型。
// 最简单的恒等函数:
const identity = (value) => value;
// 对应到类型层面:
type Identity<Type> = Type;
// 使用方式如下:
type Result = Identity<number>;
// 最终类型 Result = number
// 更复杂的例子:
const createObject = (a, b) => ({ a, b });
// 对应到类型层面:
type CreateObject<A, B> = { a: A, b: B };
// 使用方式如下:
type Result = CreateObject<string, boolean>;
// 最终类型 Result = { a: string; b: boolean }
为泛型类型函数添加类型约束
extends 关键字用于告知 TypeScript,某个类型参数必须是指定类型的子类型,这和为函数参数指定类型类似。
type CreateObject<A extends string, B> = { a: A, b: B };
// A 必须是 string 类型,否则会报错
type Errored = CreateObject<number, boolean>;
// 报错信息:Type 'number' does not satisfy the constraint 'string'
type Result = CreateObject<'name', boolean>;
// 最终类型 Result = { a: "name"; b: boolean; }
为泛型类型函数添加默认类型
你也可以为泛型类型函数指定默认类型,就像为函数参数设置默认值一样。
type CreateObject<
Key extends string = 'defaultName',
Value = string
> = { [Key]: Value };
// 现在 Key 和 Value 都是可选参数了
type Result = CreateObject;
// 最终类型 Result = { defaultName: string }
type Result2 = CreateObject<'name'>;
// 最终类型 Result2 = { name: string; }
type Result3 = CreateObject<'name', boolean>;
// 最终类型 Result3 = { name: boolean; }
实战案例:简易 CRUD 类型生成器
借助上述特性,我们已经可以创建一个通用的 CRUD 类型生成器:
type Crud<Resource extends { id: string | number }> = {
// 创建操作:接收除 id 外的所有字段
create: (resource: Omit<Resource, 'id'>) => Resource;
// 查询单个:接收 id,返回资源或 undefined
getOne: (id: Resource['id']) => Resource | undefined;
// 更新操作:接收 id 和部分资源(不含 id),返回资源或 undefined
update: (
id: Resource['id'],
resource: Partial<Omit<Resource, 'id'>>
) => Resource | undefined;
// 删除操作:接收 id,返回是否删除成功的布尔值
delete: (id: Resource['id']) => boolean;
// 列表查询:接收部分资源(不含 id)作为筛选条件,返回资源数组
getList: (filter: Partial<Omit<Resource, 'id'>>) => Resource[];
}
// 使用示例
type User = { id: number; name: string; email: string };
type UserCrud = Crud<User>;
// 最终类型 UserCrud = {
// create: (resource: Omit<User, 'id'>) => User;
// getOne: (id: number) => User | undefined;
// update: (id: number, resource: Partial<Omit<User, 'id' >>) =>
// User | undefined;
// delete: (id: number) => boolean;
// getList: (filter: Partial<Omit<User, 'id'>>) => User[];
// }
条件判断:条件类型
我们已经有了 “函数”,那能实现条件判断吗?当然可以。通过 extends 关键字在类型函数中创建条件判断 ——extends 用于检测一个类型是否是另一个类型的子类型,再结合三元表达式语法:条件 ? 成立时的类型 : 不成立时的类型。
type IsNumber<Value extends unknown> =
Value extends number ? true : false;
type Result = IsNumber<7>;
// 最终类型 Result = true
type Result2 = IsNumber<'seven'>;
// 最终类型 Result2 = false
实战案例:从事件中提取事件类型
type CreateEvent = {
type: "create";
payload: { name: string }
};
type UpdateEvent = {
type: "update";
payload: { id: number; name?: string }
};
type DeleteEvent = {
type: "delete";
payload: { id: number }
};
type UnknownEvent = unknown;
type Event = CreateEvent | UpdateEvent | DeleteEvent | UnknownEvent;
type InferEventType<T extends Event> = T extends CreateEvent
? "create"
: T extends UpdateEvent
? "update"
: T extends DeleteEvent
? "delete"
: never;
// 使用示例
type EventType = InferEventType<UpdateEvent>; // 'update'
type EventType2 = InferEventType<CreateEvent>; // 'create'
type EventType3 = InferEventType<DeleteEvent>; // 'delete'
type EventType4 = InferEventType<'An event'>; // never
infer 关键字:类型世界的 “变量”
infer 关键字用于在类型定义中创建 “变量”,支持解构赋值。
// 首先,使用 infer 关键字解构数组的第一个元素并返回
type First<Element> =
Element extends [infer FirstElement, ...any[]] ? FirstElement : never;
// 使用示例
type Result = First<[string, number, boolean]>;
// 最终类型 Result = string
type Result2 = First<[]>;
// 最终类型 Result2 = never
实战案例
借助 infer,我们可以简化条件类型章节中的事件类型提取示例:
type InferEventType<
T extends Event
> = T extends { type: infer EventType } ? EventType : never;
// 我们定义了一个变量 EventType,用于提取 T 中的 type 字段
// 无需再逐个检查每种事件类型
// 使用示例
type EventType = InferEventType<UpdateEvent>; // 'update'
type EventType2 = InferEventType<CreateEvent>; // 'create'
type EventType3 = InferEventType<DeleteEvent>; // 'delete'
type EventType4 = InferEventType<{}>; // never
递归类型:类型世界的 “循环”
一个类型可以调用自身,这使得我们能够创建递归类型。
你可以用递归类型来定义递归数据结构,比如树形结构:
type TreeNode<Value> = {
value: Value;
children?: TreeNode<Value>[];
}
type StringTree = TreeNode<string>;
const tree: StringTree = {
value: 'root',
children: [
{ value: 'child1' },
{ value: 'child2', children: [
{ value: 'grandchild1' }
] }
]
};
但你也可以用递归遍历数组。让我们实现一个 Find 类型,用于在数组中查找指定类型,找到则返回该类型,否则返回 never:
type Find<ArrayType extends unknown[], ValueType> =
// 将数组解构为第一个元素和剩余元素
ArrayType extends [infer First, ...infer Rest]
// 检查第一个元素是否是我们要找的类型
? First extends ValueType
// 若是,返回该元素类型
? First
// 若不是,递归处理剩余数组
: Find<Rest, ValueType>
: never; // 若数组为空,返回 never
// 事件数组使用示例
type EventsArray = [{
type: 'create';
payload: { name: string; };
}, {
type: 'update';
payload: { id: number; name?: string; };
}, {
type: 'delete';
payload: { id: number; };
}];
type CreateEvent = Find<
EventsArray,
{ type: 'create'; payload: { name: string; }; }
>;
// 最终类型 CreateEvent = { type: "create"; payload: { name: string; }; }
type UpdateEvent = Find<
EventsArray,
{ type: 'update'; payload: { id: number; name?: string; }; }
>;
// 最终类型 UpdateEvent = {
// type: "update";
// payload: { id: number; name?: string | undefined; };
// }
type DeleteEvent = Find<
EventsArray,
{ type: 'delete'; payload: { id: number; }; }
>;
// 最终类型 DeleteEvent = { type: "delete"; payload: { id: number; }; }
type UnknownEvent = Find<
EventsArray,
{ type: 'unknown'; payload: {}; }
>;
// 最终类型 UnknownEvent = never
实战案例:为中间件结果定义类型
你可能会好奇这在实际开发中有什么用。想象一个中间件系统,中间件会根据操作参数修改操作结果。已知中间件类型,我们需要找到特定参数对应的结果类型:
// 中间件类型定义:
// 接收 Arg 类型的参数,返回 Result 类型的结果
type Middleware<Arg, Result> = (arg: Arg) => Result;
// 示例中间件集合
type Middlewares = [
Middleware<
{ type: "getList"; withComments: true },
{ id: number; name: string; comments: string[] }[]
>,
Middleware<{ type: "getList" }, { id: number; name: string }[]>,
Middleware<
{ type: "getOne"; withComments: true },
{ id: number; name: string; comments: string[] }
>,
Middleware<{ type: "getOne" }, { id: number; name: string }>
];
// 查找参数类型匹配 Target 的中间件
type FindMiddleware<Target> = Find<Middlewares, Middleware<Target, any>>;
// 获取中间件的结果类型
type GetResult<MiddlewareInput extends Middleware<any, any>> =
MiddlewareInput extends Middleware<any, infer Result> ? Result : never;
// 组合所有逻辑:根据参数获取中间件结果类型
type GetMiddlewareResult<Arg> = GetResult<FindMiddleware<Arg>>;
type ListResult = GetMiddlewareResult<{ type: "getList" }>;
// 最终类型 ListResult = { id: number; name: string; }[]
type OneResult = GetMiddlewareResult<{ type: "getOne" }>;
// 最终类型 OneResult = { id: number; name: string; }
type ListWithCommentsResult = GetMiddlewareResult<{
type: "getList";
withComments: true;
}>;
// 最终类型 ListWithCommentsResult = {
// id: number;
// name: string;
// comments: string[]
// }[]
type OneWithCommentsResult = GetMiddlewareResult<{
type: "getOne";
withComments: true;
}>;
// 最终类型 OneWithCommentsResult = {
// id: number;
// name: string;
// comments: string[]
// }
字符串操作:模板字面量类型
TypeScript 允许你在类型层面使用模板字面量操作字符串。
你可以执行简单的字符串拼接:
type HelloWorld<Greeted extends string = 'world'> = `Hello ${Greeted}`;
type DefaultResult = HelloWorld; // 'Hello world'
type Result = HelloWorld<'TypeScript'>; // 'Hello TypeScript'
你也可以执行更复杂的字符串操作,例如结合 infer 关键字和递归移除字符串中的所有空格:
type RemoveWhitespace<S extends string> =
S extends `${infer First} ${infer Rest}`
? `${First}${RemoveWhitespace<Rest>}`
: S;
type Result = RemoveWhitespace<'Hello World !'>;
// 最终类型 Result = "HelloWorld!"
实战案例:根据属性名生成 Getter 方法名
type Getter<Key extends string> = `get${Capitalize<Key>}`;
type Result = Getter<'name'>;
// 最终类型 Result = "getName"
type Result2 = Getter<'firstName'>;
// 最终类型 Result2 = "getFirstName"
注:
Capitalize是 TypeScript 内置的工具类型,用于将字符串类型的首字母大写。你也可以借助Uppercase和递归自己实现:type Capitalize< S extends string > = S extends `${infer First}${infer Rest}` ? `${Uppercase<First>}${Rest}` : S; // Uppercase 也是一个工具类型 // 但其底层是通过编译器内置逻辑实现的 // 使用示例 type Result = Capitalize<"hello">; // 最终类型 Result = "Hello"
映射类型
借助递归,我们可以遍历数组,但如何遍历对象属性呢?映射类型允许你通过转换现有类型的每个属性来创建新类型。映射类型基于索引访问类型和 keyof 运算符的语法构建。
索引访问类型
索引访问类型允许你通过 Type[Key] 语法获取对象类型中某个属性的类型:
type User = { id: number; name: string; email: string };
type UserId = User['id'];
// 最终类型 UserId = number
type UserName = User['name'];
// 最终类型 UserName = string
它也可以结合键的联合类型,获取这些键对应的类型联合:
type UserIdOrName = User['id' | 'name'];
// 最终类型 UserIdOrName = number | string
keyof 运算符
keyof 运算符允许你获取一个类型的所有键组成的联合类型。
type User = { id: number; name: string; email: string };
type UserKeys = keyof User;
// 最终类型 UserKeys = "id" | "name" | "email"
in 关键字
最后,通过添加 in 关键字,我们可以遍历一个类型的所有键。
type UserPropertiesAsString = {
// 这里 K 是 User 的每个键
// 我们遍历每个键,创建一个返回该属性类型(User[K])的函数
[K in keyof User]: () => User[K];
};
// 最终类型 UserPropertiesAsString = {
// id: () => number;
// name: () => string;
// email: () => string;
// }
借助这一特性,你可以创建一个将对象所有方法转换为返回 Promise 的类型:
type Promisify<R extends Record<string, (...args: any) => any>> = {
[K in keyof R]:
(...args: Parameters<R[K]>) => Promise<ReturnType<R[K]>>;
};
type Input = {
getName: () => string;
getById: (id: string) => { id: string; name: string };
};
type PromisifiedInput = Promisify<Input>;
// 最终类型 PromisifiedInput = {
// getName: () => Promise<string>;
// getById: (id: string) => Promise<{
// id: string;
// name: string;
// }>;
// }
实战案例
以下是一个更复杂的映射类型示例:接收一个资源类型和一组筛选键,为每个筛选键生成 getByFilterType 方法:
type Product = {
id: number;
name: string;
quantity: number;
inStock: boolean;
};
type CrudWithFilters<
Resource extends { id: string | number },
FilterKeys extends keyof Resource
> = {
// 为每个筛选键生成 getByXXX 方法
[K in FilterKeys as `getBy${Capitalize<string & K>}`]:
(value: Resource[K]) => Resource[];
}
type ProductCrudWithFilters = CrudWithFilters<
Product,
'name' | 'quantity' | 'inStock'
>;
// 最终类型 ProductCrudWithFilters = {
// getByName: (value: string) => Product[];
// getByQuantity: (value: number) => Product[];
// getByInStock: (value: boolean) => Product[];
// }
// 实际函数使用示例
const productCrud: CrudWithFilters<
Product,
'name' | 'quantity' | 'inStock'
> = {
getByName: (name: string) => [
{ id: 1, name, quantity: 10, inStock: true }
],
getByQuantity: (quantity: number) => [
{ id: 2, name: 'Product 2', quantity, inStock: false }
],
getByInStock: (inStock: boolean) => [
{ id: 3, name: 'Product 3', quantity: 5, inStock }
],
};
// productCrud 的类型会被正确推断
// 若函数实现与预期类型不匹配,会触发类型错误
const erroredProductCrud: CrudWithFilters<
Product,
'name' | 'quantity' | 'inStock'
> = {
getByName: (name: number) => [
{ id: 1, name: 'Product 1', quantity: 10, inStock: true }
],
// 错误信息:
// 类型 '(name: number) => { id: number; name: string; quantity: number; inStock: true; }[]'
// 不能赋值给类型 '(value: string) => Product[]'
// 参数 'name' 和 'value' 的类型不兼容
// 类型 'string' 不能赋值给类型 'number'
getByQuantity: (quantity: string) =>
[{ id: 2, name: 'Product 2', quantity: 5, inStock: false }],
// 错误信息:
// 类型 '(quantity: string) => { id: number; name: string; quantity: number; inStock: false; }[]'
// 不能赋值给类型 '(value: number) => Product[]'
// 参数 'quantity' 和 'value' 的类型不兼容
// 类型 'number' 不能赋值给类型 'string'
getByInStock: (inStock: boolean) => 'not found',
// 错误信息:
// 类型 '(inStock: boolean) => string'
// 不能赋值给类型 '(value: boolean) => Product[]'
// 类型 'string' 不能赋值给类型 'Product[]'
};
将函数对象简化为单个函数
借助 keyof,我们可以根据对象的结构返回任意类型,而不仅仅是对象。例如,以下函数将一个函数对象转换为单个函数:第一个参数是对象的键,后续参数是对应函数的参数。
// 首先定义辅助类型:
// ObjToFunc 接收任意函数对象,将其转换为单个函数
type ObjectToFunc<Obj extends Record<string, (...args: any) => any>> = {
// 遍历函数对象的键
// 为每个键定义一个函数
<K extends keyof Obj>(key: K, ...args: Parameters<Obj[K]>): ReturnType<
Obj[K]
>;
};
// 结果是对象每个键对应的函数组成的联合类型
type Example = ObjectToFunc<
{
getStringLength: (s: string) => number;
isEven: (n: number) => boolean;
}
>;
// 最终类型 Example =
// | (key: 'getStringLength', s: string) => number
// | (key: 'isEven', n: number) => boolean;
// 现在实现实际函数
const transformObjectToFunction = <
Obj extends Record<string, (...args: any) => any>
>(
obj: Obj
): ObjectToFunc<Obj> => {
return ((key: string, ...args: unknown[]) => {
const func = obj[key];
return func(...args);
}) as ObjectToFunc<Obj>;
};
// 使用示例:将多个资源的 getList 函数组合为单个 getList 函数
const getList = transformObjectToFunction({
authors: (filter: { name?: string }) => [
{ id: 1, name: "Author 1" },
{ id: 2, name: "Author 2" },
],
posts: (filter: { authorId?: number; published?: boolean }) => [
{ id: 1, title: "A post" },
],
comments: (filter: { postId?: number; authorId?: number }) => [
{ id: 1, content: "A comment" },
],
});
getList("posts", { authorId: 1 });
// 无错误
// 推断类型为:
// const getList: <"posts">(key: "posts", filter: {
// authorId?: number;
// published?: boolean;
// }) => {
// id: number;
// title: string;
// }[]
getList("posts", { name: 'Author 1' });
// 错误:对象字面量只能指定已知属性,
// 而 'name' 不在类型 '{ authorId?: number | undefined; published?: boolean | undefined; }' 中
getList("authors", { name: "Author 1" });
// 无错误
// 推断类型为:
// const getList: <"authors">(key: "authors", filter: {
// name?: string | undefined;
// }) => {
// id: number;
// name: string;
// }[]
注:上述示例使用了 TypeScript 提供的工具类型
ReturnType和Parameters。>ReturnType返回给定函数类型的返回值类型,如果你好奇其实现方式,可以参考以下代码:type ReturnType<T extends (...args: any) => any> = T extends ( ...args: any ) => infer R ? R : any;
Parameters返回给定函数类型的参数类型组成的元组,其实现方式如下:type Parameters<T extends (...args: any) => any> = T extends ( ...args: infer P ) => any ? P : never;
总结
将 TypeScript 类型定义当作编程语言对待,让我们能够定义复杂的泛型类型。这些泛型类型进而支持我们编写更通用的代码,减少重复,并提升整个代码库的类型安全性。
核心要点总结:
- 泛型类型是转换类型的 “函数”
- 条件类型(
extends ? :)支持分支逻辑 infer关键字类似变量赋值,允许通过解构从其他类型中提取值- 递归支持遍历数组和复杂类型
- 模板字面量支持在类型层面操作字符串
- 映射类型支持遍历对象属性
借助这些工具,你可以创建适配自身需求的复杂类型工具,在编译期捕获错误,并在 IDE 中获得出色的自动补全体验。下次编写类型定义时,不妨把它当作编写程序 —— 因为这正是你正在做的事情。