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开发干货

相关推荐
架构师老Y33 分钟前
003、Python Web框架深度对比:Django vs Flask vs FastAPI
前端·python·django
小陈工3 小时前
Python Web开发入门(十七):Vue.js与Python后端集成——让前后端真正“握手言和“
开发语言·前端·javascript·数据库·vue.js·人工智能·python
xiaotao1318 小时前
第九章:Vite API 参考手册
前端·vite·前端打包
午安~婉8 小时前
Electron桌面应用聊天(续)
前端·javascript·electron
彧翎Pro8 小时前
基于 RO1 noetic 配置 robosense Helios 32(速腾) & xsense mti 300
前端·jvm
小码哥_常8 小时前
解锁系统设置新姿势:Activity嵌入全解析
前端
之歆9 小时前
前端存储方案对比:Cookie-Session-LocalStorage-IndexedDB
前端
哟哟耶耶9 小时前
vue3-单文件组件css功能(:deep,:slotted,:global,useCssModule,v-bind)
前端·javascript·css
是罐装可乐9 小时前
深入理解“句柄(Handle)“:从浏览器安全到文件系统访问
前端·javascript·安全
华科易迅9 小时前
Vue如何集成封装Axios
前端·javascript·vue.js