TypeScript类型映射中的-?移除可选修饰符的用法
在 TypeScript 中处理对象类型时,经常会遇到属性为可选的情况。但在某些业务场景下(如表单提交前的最终校验),我们需要强制要求这些属性必须存在。手动重新定义一个类型不仅繁琐,还容易导致代码冗余。TypeScript 的映射类型提供了 -? 修饰符,能够高效地将可选属性转换为必需属性。
1. 定义基础可选类型
首先,我们需要一个包含可选属性的基础类型,以便演示如何移除其可选修饰符。
定义一个用户信息接口 UserPartial,其中 name 是必需的,而 age 和 email 是可选的。
interface UserPartial {
id: number;
name: string;
age?: number;
email?: string;
}
此时,如果你尝试创建一个缺少 name 的对象,TypeScript 会报错,但缺少 age 或 email 则不会。
2. 理解映射类型语法
要修改属性的修饰符,必须使用映射类型的语法。其核心结构是对键名进行遍历,并在遍历过程中应用修饰符。
分析以下映射类型的基础结构:
type MappedType<T> = {
[P in keyof T]: T[P];
};
在这个结构中:
T是传入的泛型(即上一步的UserPartial)。[P in keyof T]表示遍历T中所有的键名。T[P]表示获取该键名对应的原始类型。
默认情况下,这种写法会保留属性原有的 readonly 和 ?(可选)修饰符。要移除这些修饰符,需要在键名遍历部分显式添加操作符。
3. 使用 -? 移除可选修饰符
-? 操作符的作用是告诉 TypeScript:“在生成新类型时,移除键名的可选标记”。这相当于将 key?: Type 转换为 key: Type。
编写一个工具类型 RequiredMy,利用 -? 将所有属性变为必需:
type RequiredMy<T> = {
[P in keyof T]-?: T[P];
};
注意符号的位置,-? 紧跟在遍历变量 P 的后面,且位于中括号 [] 内部。- 号表示减去,? 表示可选修饰符。
4. 验证类型转换效果
将上一步定义的 UserPartial 传入 RequiredMy,生成一个新的全必需类型,并进行验证。
声明一个新的类型别名 UserStrict,应用该工具类型:
type UserStrict = RequiredMy<UserPartial>;
尝试创建一个对象来测试类型约束:
// ❌ 错误:属性 'age' 是必需的,但在类型 '{ id: number; name: string; }' 中缺失。
const testUser1: UserStrict = {
id: 1,
name: "Alice"
};
// ✅ 正确:所有属性都已提供
const testUser2: UserStrict = {
id: 1,
name: "Alice",
age: 30,
email: "alice@example.com"
};
此时,TypeScript 会强制检查 age 和 email,如果缺少任何一个,代码编译将无法通过。
5. 对比内置 Required 工具类型
实际上,TypeScript 标准库中已经内置了实现相同功能的 Required 工具类型。理解自定义实现有助于掌握映射类型的底层逻辑。
查看标准库中 Required 的定义(逻辑与上述 RequiredMy 完全一致):
/**
* Make all properties in T required
*/
type Required<T> = {
[P in keyof T]-?: T[P];
};
在实际项目中,通常直接使用内置的 Required 即可,无需重复造轮子。
6. 处理嵌套对象的强制必需
在某些复杂的数据结构中,可选属性可能本身就是一个对象。直接使用 -? 只能处理第一层的属性,无法递归处理深层对象。如果需要深度移除可选修饰符,需要结合条件类型进行递归处理。
编写一个深度必需类型 DeepRequired:
type DeepRequired<T> = {
[P in keyof T]-?: DeepRequiredNonObject<T[P]>;
};
// 辅助类型:如果是对象则继续递归,否则直接返回类型
type DeepRequiredNonObject<T> = T extends object
? DeepRequired<T>
: T;
定义一个包含嵌套对象的复杂接口:
interface Company {
name: string;
address?: {
city?: string;
street?: string;
};
}
应用 DeepRequired 进行转换:
type CompanyStrict = DeepRequired<Company>;
const myCompany: CompanyStrict = {
name: "Tech Corp",
address: {
// ❌ 错误:属性 'city' 和 'street' 现在也是必需的
// city: "New York"
}
};
通过这种方式,-? 的威力被扩展到了整个对象树,确保了数据结构的完整性。
7. 修饰符组合使用总结
在映射类型中,修饰符可以组合使用。除了 -?(移除可选),还有 +?(添加可选)、-readonly(移除只读)和 +readonly(添加只读)。默认行为通常是 +readonly 和 +?(如果原属性是可选的,则保留;如果是必需的,保留必需)。
下表展示了不同修饰符对属性类型的影响:
| 修饰符组合 | 作用描述 | 示例结果 |
|---|---|---|
+? (默认) |
保留或添加可选性 | key?: Type |
-? |
移除可选性,变为必需 | key: Type |
-readonly |
移除只读限制 | 允许修改 key |
编写一个同时修改只读和可选性的类型示例:
type MutableRequired<T> = {
-readonly [P in keyof T]-?: T[P];
};
interface ReadonlyPartial {
readonly id?: number;
readonly name?: string;
}
// 结果:id 和 name 变为非只读且必需
type FinalType = MutableRequired<ReadonlyPartial>;
暂无评论,快来抢沙发吧!