TypeScript 类型魔法:像遍历对象一样改造你的类型

一、核心语法

typescript 复制代码
type MappedType<T> = {
    [P in keyof T]: T[P];
};

解析:

  1. keyof T : 这是索引类型查询操作符 。它会获取类型 T 的所有公共属性名,并创建一个由这些属性名组成的 字符串字面量联合类型(不清楚的可以查看之前的文章)。
  2. P in keyof T : 这就是遍历 部分。它会遍历 keyof T 这个联合类型中的每一个成员。P 在每次迭代中都会被绑定到联合类型的一个成员上。
  3. 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 键会被自动丢弃。

这就是 PickOmit 这类工具类型背后的实现原理之一!

总结

如果你喜欢本教程,记得点赞+收藏!关注我获取更多JavaScript/TypeScript开发干货

相关推荐
程序员黑豆2 小时前
Java类型推断完全指南:从var到菱形运算符,掌握使用限制与最佳实践
java·前端·ai编程
To_OC3 小时前
踩了个 TS 的坑之后,我终于把 type 和 interface 掰明白了
前端·react.js·typescript
GreenTea3 小时前
深度解读 Anthropic 多智能体报告:更强的模型 ≠ 更好的协调
前端·后端·算法
浮生望3 小时前
前端API工程化:用 Mock 数据与 Axios 配置实现独立于后端的并行开发
前端
万少4 小时前
给 DeepSeek Harness 装个"应用商店":一条命令,595 个插件随你逛
前端·javascript·后端
波波0074 小时前
C# 15 重磅新特性: 带标签 break 与 continue:重新定义嵌套循环控制流
服务器·前端·c#
Brown.alexis5 小时前
es6知识点1-自备使用
前端·ecmascript·es6
小爬的老粉丝6 小时前
JavaScript 纯前端预览 WPS:先识别容器,再路由解析器
前端·javascript·wps
mCell6 小时前
用 Cordis 从零构建一个 Mini DeepSeek Harness
typescript·agent·deepseek
计算机魔术师6 小时前
新兴多智能体系统的模式与问题
前端