一、核心语法
typescript
type MappedType<T> = {
[P in keyof T]: T[P];
};
解析:
keyof T
: 这是索引类型查询操作符 。它会获取类型T
的所有公共属性名,并创建一个由这些属性名组成的 字符串字面量联合类型(不清楚的可以查看之前的文章)。P in keyof T
: 这就是遍历 部分。它会遍历keyof T
这个联合类型中的每一个成员。P
在每次迭代中都会被绑定到联合类型的一个成员上。T[P]
: 这是索引访问类型 。它会获取类型T
中属性P
对应的类型。
示例:
typescript
interface User {
id: number;
name: string;
email: string;
}
// 手动创建可选类型
interface PartialUser {
id?: number;
name?: string;
email?: string;
}
type MappedType<T> = {
[P in keyof T]: T[P];
};
type cloneUser = MappedType<User>; // 拷贝一份同样定义的User接口
二、添加与移除修饰符
映射类型的真正威力在于,我们可以在遍历属性时添加或移除**只读(readonly
)和 可选(?
)**修饰符。
2.1 创建只读类型 (Readonly<T>
)
我们只需要在属性定义前加上 readonly
关键字,就可以为所有属性加上readonly
修饰符:
typescript
type MyReadonly<T> = {
readonly [P in keyof T]: T[P];
};
const user: User = { id: 1, name: 'Alice', email: 'a@b.com' };
type ReadonlyUser = MyReadonly<User>;
const readonlyUser: ReadonlyUser = user;
readonlyUser.id = 2; // 错误:无法为"id"赋值,因为它是只读属性
2.2 创建可选类型 (Partial<T>
)
同样地,要实现 Partial<T>
,我们只需在属性名后加上 ?
即可:
typescript
type MyPartial<T> = {
[P in keyof T]?: T[P];
};
type PartialUser = MyPartial<User>;
const partialUser: PartialUser = {
name: 'Bob', // 只提供部分属性,完全合法
};
2.3 移除修饰符:-
操作符
我们不仅可以添加修饰符,还可以通过在修饰符前加上 -
来移除它们。
例如,实现一个 Mutable<T>
类型,它可以移除所有属性的 readonly
标志:
typescript
type MyMutable<T> = {
-readonly [P in keyof T]: T[P];
};
type MutableUser = MyMutable<ReadonlyUser>;
const mutableUser: MutableUser = {
id: 1,
name: 'Charlie',
email: 'c@d.com',
};
mutableUser.id = 100; // 合法! `readonly` 被移除了。
type MyRequired<T> = {
[P in keyof T]-?: T[P];
};
type RequiredUser = MyRequired<User>; // 可选属性被删除
const requiredUser: RequiredUser = { // 类型"{ id: number; }"缺少类型"MyRequired<User>"中的以下属性: name, email
id: 1,
};
三、重映射键名 (as
子句)
可以通过as
更改已有属性
3.1 为所有属性创建 Getter 方法
假设我们想创建一个新类型,它将 User
的每个属性 prop
都转换成一个名为 getProp
的方法,该方法返回原属性的值。
typescript
// Capitalize 是 TypeScript 内置的工具类型,用于将字符串首字母大写
type Getters<T> = {
[P in keyof T as `get${Capitalize<string & P>}`]: () => T[P];
};
// string & P 是为了确保 P 是字符串类型,以便 Capitalize 可以处理它
type UserGetters = Getters<User>;
/*
UserGetters 的类型是:
{
getId: () => number;
getName: () => string;
getEmail: () => string;
}
*/
这里的魔法在于 as \
get${Capitalize<string & P>}``。
- 我们使用了模板字面量类型来动态构建新的属性名。
- 当
P
是'name'
时,新的键名就是'getName'
。 - 同时,我们将属性值的类型从
T[P]
改为了() => T[P]
,即一个返回原属性值的函数。
3.2 过滤属性
as
子句还有一个绝活:通过返回 never
类型来过滤掉不想要的属性。 假设我们只想保留 User
类型中值为 string
类型的属性:
typescript
type StringPropertiesOnly<T> = {
[P in keyof T as T[P] extends string ? P : never]: T[P];
};
type UserStringProps = StringPropertiesOnly<User>;
/*
UserStringProps 的类型是:
{
name: string;
email: string;
}
// 'id' 属性因为类型不是 string,所以被过滤掉了
*/
解析:
- 我们用了一个条件类型
T[P] extends string ? P : never
。 - 如果属性
P
的值类型T[P]
是string
类型,那么键名就保留P
。 - 否则,键名就变成
never
。在映射类型中,never
键会被自动丢弃。
这就是 Pick
和 Omit
这类工具类型背后的实现原理之一!
总结
如果你喜欢本教程,记得点赞+收藏!关注我获取更多JavaScript/TypeScript开发干货