TypeScript 高级类型:创建部分可选属性的工具类型
1. 问题场景
在 TypeScript 开发中,我们经常遇到这样的情况:一个数据对象的创建类型(Create)与它的基本类型(Article)非常相似,但某些属性在创建时是可选的。
例如,我们有一个 Article 接口:
interface Article {
title: string;
content: string;
author: string;
date: Date;
readCount: number;
}在创建一个 createArticle 函数时,我们希望 author、date 和 readCount 是可选的。如果手动编写这个创建选项的类型,会产生大量重复代码:
// 创建文章时,作者、日期、阅读量是可选的
interface CreateArticleOptions {
title: string; // 必选
content: string; // 必选
author?: string; // 可选
date?: Date; // 可选
readCount?: number; // 可选
}这种方式不仅繁琐,而且当 Article 接口更新时,我们还需要手动同步更新 CreateArticleOptions,非常不利于维护。
2. 解决方案:自定义 Optional 工具类型
理想的解决方案是能根据 Article 类型自动“演算”出 CreateArticleOptions 类型。我们可以创建一个自定义的工具类型 Optional<T, K> 来实现这个目标。
T: 原始类型(如Article)。K: 需要从T中挑选出来并设为可选的属性名集合(如'author' | 'date' | 'readCount')。
实现代码
这个强大的工具类型只需要一行代码即可实现:
type Optional<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>;使用示例
有了 Optional 工具,我们可以像下面这样生成 CreateArticleOptions:
// 原始文章类型
interface Article {
title: string;
content: string;
author: string;
date: Date;
readCount: number;
}
// 1. 定义工具类型 Optional
type Optional<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>;
// 2. 使用 Optional 生成创建选项类型
type CreateArticleOptions = Optional<Article, 'author' | 'date' | 'readCount'>;
/*
CreateArticleOptions 的推断结果等同于:
{
title: string;
content: string;
} & {
author?: string;
date?: Date;
readCount?: number;
}
最终合并为:
{
title: string;
content: string;
author?: string;
date?: Date;
readCount?: number;
}
*/
// 3. 在函数中使用
function createArticle(options: CreateArticleOptions) {
// ...
}这样,CreateArticleOptions 就和 Article 建立了关联,当 Article 变化时,它也能自动更新,大大提升了代码的可维护性。
3. 核心知识点:内置工具类型解析
要理解 Optional 的工作原理,需要先掌握几个 TypeScript 内置的核心工具类型和一个关键的类型操作符。
3.1. 交叉类型 (Intersection Types): &
交叉类型 & 用于将多个类型合并为一个新类型。新类型将拥有所有成员类型的所有属性。
interface A {
name: string;
}
interface B {
age: number;
}
type C = A & B;
// C 的类型为: { name: string; age: number; }
const person: C = { name: "Alice", age: 30 };3.2. Omit<T, K>
Omit<T, K>(缺省)会创建一个新类型,该类型拥有 T 的所有属性,但移除了指定的属性 K。
interface Todo {
title: string;
description: string;
completed: boolean;
}
// 移除 'description' 和 'completed' 属性
type TodoPreview = Omit<Todo, 'description' | 'completed'>;
// TodoPreview 的类型为: { title: string; }
const todo: TodoPreview = { title: "Clean room" };3.3. Pick<T, K>
Pick<T, K>(挑选)会创建一个新类型,该类型只包含从 T 中挑选出来的属性 K。
interface Todo {
title: string;
description: string;
completed: boolean;
}
// 只挑选 'title' 和 'completed' 属性
type TodoInfo = Pick<Todo, 'title' | 'completed'>;
// TodoInfo 的类型为: { title: string; completed: boolean; }
const todo: TodoInfo = { title: "Buy groceries", completed: false };3.4. Partial<T>
Partial<T>(部分)会创建一个新类型,该类型将 T 的所有属性都变为可选的。
interface Todo {
title: string;
description: string;
}
// 将所有属性变为可选
type PartialTodo = Partial<Todo>;
// PartialTodo 的类型为: { title?: string; description?: string; }
const update: PartialTodo = { title: "Learn TypeScript" };4. Optional<T, K> 实现原理详解
了解了上述基础知识后,我们再来拆解 Optional 的实现:
type Optional<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>;
整个过程分为两部分,最后通过 & 合并:
Omit<T, K>:保留必选属性- 这一部分从原始类型
T中移除了我们希望设为可选的属性K。 - 结果是一个只包含所有必选属性的新类型。
- 示例:
Omit<Article, 'author' | 'date' | 'readCount'>的结果是{ title: string; content: string; }。
- 这一部分从原始类型
Partial<Pick<T, K>>:创建可选属性- 这部分由内到外执行:
Pick<T, K>:首先,从原始类型T中挑选出那些我们希望设为可选的属性K。- 示例:
Pick<Article, 'author' | 'date' | 'readCount'>的结果是{ author: string; date: Date; readCount: number; }。
- 示例:
Partial<...>:然后,使用Partial将上一步挑选出的所有属性都变为可选。- 示例:
Partial<...>应用后的结果是{ author?: string; date?: Date; readCount?: number; }。
- 示例:
- 这部分由内到外执行:
&:合并结果- 最后,使用交叉类型
&将第一步得到的“必选属性”类型和第二步得到的“可选属性”类型合并起来。 - 最终结果:typescript合并后即为我们想要的最终类型:
{ title: string; content: string; } & { author?: string; date?: Date; readCount?: number; }typescript{ title: string; content: string; author?: string; date?: Date; readCount?: number; }
- 最后,使用交叉类型
通过这种组合方式,我们仅用一行代码就实现了一个灵活、可复用且功能强大的高级工具类型,完美解决了最初的问题。