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 小时前
Chrome GPU 加速优化配置(前端 3D 可视化 / 数字孪生专用)
前端·chrome·3d
今天不要写bug2 小时前
antv x6实现封装拖拽流程图配置(适用于工单流程、审批流程应用场景)
前端·typescript·vue·流程图
luquinn2 小时前
实现统一门户登录跳转免登录
开发语言·前端·javascript
用户21411832636023 小时前
dify案例分享-5分钟搭建智能思维导图系统!Dify + MCP工具实战教程
前端
augenstern4163 小时前
HTML(面试)
前端
excel3 小时前
前端常见布局误区:1fr 为什么撑爆了我的容器?
前端
vayy3 小时前
uniapp中 ios端 scroll-view 组件内部子元素z-index失效问题
前端·ios·微信小程序·uni-app
专注API从业者3 小时前
基于 Node.js 的淘宝 API 接口开发:快速构建异步数据采集服务
大数据·前端·数据库·数据挖掘·node.js
前端无冕之王3 小时前
一份兼容多端的HTML邮件模板实践与详解
前端·css·数据库·html