TS 类型体操 之 循环中的键值判断,as 关键字使用

本文解析了TypeScript中几个典型题目,包括Omit、Readonly2、PickByType及PartialByKeys的实现方法,重点讲解了as关键字及&运算符的使用技巧。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

TS 类型体操 之 循环中的键值判断,as 关键字使用

这里将要讲几个题目,非常的有代表性,而且这些题目用到了很多的关键字和 TS 语法

3・实现 Omit 8・Readonly 2 2595・PickByType 2757・PartialByKeys

  • 如何在对象的循环中给键值做判断
  • P in keyof as any extends P 是什么意思
  • 如果一定要用到 & 符,如何在返回之前合并 & 2 边的内容

上面挑选的几题,其实解题套路都一样,了解了上面 3 个问题后,这几题套模版就能解决

00003-medium-omit 题目实现

需求: Omit 会创建一个省略 K 中字段的 T 对象。

这和 Pick 很像,只是结果相反,Pick 是挑选需要的字段,而 Omit 则是排除指定的字段

  • 根据现了解的关键字, keyof 肯定要用,可是怎么排除其他的键?
    never 来做键,就能在这个字段值排除了

所以就引出了第一个问题 如何在对象的循环中给键值做判断

先看答案:

type MyOmit<T, K extends keyof T> = {
  [P in keyof T as P extends K ? never : P]: T[P]
}

K extends keyof T 这些语法就不解释了,和 Pick 一样,如果不了解的可以看看第一篇文章,TS 体操的前置知识

关键就在于 P extends K ? never : P 判断 P 这个键是否属于 K 的范围,属于的则是要排除的,返回 nerver,不属于的就需要返回当前的 P,代表把当前的键保留下来


看到这里又引出第二个问题 P in keyof as any extends P 是什么意思

加一些括号好理解一下

[
  (P in keyof T) as
  (P extends K ? never : P)
]

以 as 为分界,把这段代码分为 2 段

  • P in keyof T 是遍历的意思,这个好理解
  • as 在这里姑且认定为是 断言
  • P extends K ? never : P 这里是为了判断类型

连起来怎么看

  • P 在 T 的范围循环
  • P 得到的是 T 中的键
  • 对于这个 P 我们为他断言 为 P / never
  • 如果 P 的这个键在 K 的范围中,我们就断言当前的 P 是 never(抛弃原先 P 的值),那么在对象循环的时候 never 就会被忽略掉,从而实现 Omit

00008-medium-readonly-2 题目详解

需求:这个就是 readonly 的 plus 版,指定字段来 readonly,如果没指定的则当作全部 readonly

好家伙,刚学完 Pick 和 Omit,又是指定字段,这不是手到擒来吗,用上 & 合并一下就完事了

下面是错误示范:

当时的错误思路是这样的:既然是指定字段 readonly,那我拿 原对象 和 指定字段循环出来的对象合并一下,循环的对象加个 readonly,让后面的字段覆盖前面的,那不就达成效果了吗

// 错误示范
type MyReadonly21<T, K extends keyof T = keyof T> = T & {
  readonly [P in K]: T[P]
}

结果肯定是报错的,因为 & 运算符计算出来的是 交集 ,简单点用段代码来说就是

type testReadonly = { title: string; name: string } & { readonly title: string; name: string }

// test 会报错:提示缺少了title字段
// Property 'title' is missing in type '{ name: string; }' but required in type '{ title: string; name: string; }'.
const test: testReadonly = {
  name: '111'
}

按道理 title 字段合并后应该也是只读,可是他变成了必填的了。 交集 细品,把 readonly 理解为 title 字段的一个额外的标签


正确答案如下:

用 Omit 挑选出必填的字段,然后在用 Pick 挑选出 readonly 的对象,这 2 个对象合并才是正确的答案

type MyReadonly2<T, K extends keyof T = keyof T> = {
  [P in keyof Omit<T, K>]: T[P]
} & {
  readonly [P in K]: T[P]
}

也有复杂的方案,就是假装我不会用 Omit ,用上面刚学的套路也可以解决这个问题

type MyReadonly21<T, K extends keyof T = keyof T> = {
  [P in keyof T as P extends K ? never : P]: T[P]
} & {
  readonly [P in K]: T[P]
}

02595-medium-pickbytype 题目实现

这个题目就和上面的套路一模一样!!也是需要在循环的时候就决定好哪些 key 要保留

不同的是要根据不同字段对应的类型来进行筛选,也就是说之前的
[P in keyof T as P extends U], 要换成 [P in keyof K as T[P] extends U ? nerver : P]

注意 T[P] extends U 的写法!就这么一个参数的变化,其余的该拿 P 还是拿 P,该 never 还是 never,这题就解决了

02757-medium-partialbykeys 题目详解

需求:指定字段来设置选填,如果没指定的则当作全部都选填

正是这道题,引出的问题 3: 如果一定要用到 & 符,如何在返回之前合并 & 2 边的内容

看到这个问题,是不是和 readonly2 的需求一样,无非就是把 readonly 换成 ?

代码啪一下就写完了,然后看测试用例 一个都没过

type PartialByKeys<T, K extends keyof T = keyof T> = {
  [P in keyof T as P extends K ? never : P]: T[P]
} & {
  [P in K]?: T[P]
}

可是仔细观察字段,该必填的有必填,该选填的有?。除了案例给出的是一个完整的对象,而我的是一个 {} & {} 交叉运算出来的,其他没啥区别呀

然后我仔细研究了一下测试用例,注意看 02757-partialbykeys 这题的测试用例,用的是 Equal 工具类

type cases = [
  Expect<Equal<PartialByKeys<User, 'name'>, UserPartialName>>,
  Expect<Equal<PartialByKeys<User, 'name' | 'unknown'>, UserPartialName>>,
  Expect<Equal<PartialByKeys<User, 'name' | 'age'>, UserPartialNameAndAge>>,
  Expect<Equal<PartialByKeys<User>, Partial<User>>>
]

而 08-readonl2 的测试用例,用的是 Alike

type cases = [Expect<Alike<MyReadonly21<Todo1>, Readonly<Todo1>>>]

好家伙。。如此说来, readonl2 算起来还不是完全的标准答案,顶多打个 98 分?

要解决这个问题也非常容易,我们只需要把 2 个合并一下就好了,至于怎么合并?

又有一个小妙招 —— 用 Pick,因为 Pick 能把字段都提出来,然后合并成一个新的对象,我们只需要把我们全部字段都放进去提取一次,就能合并在一起了

type Clone<T> = Pick<T, keyof T>

type PartialByKeys<T, K extends keyof T = keyof T> = Clone<
  {
    [P in keyof T as P extends K ? never : P]: T[P]
  } & {
    [P in K]?: T[P]
  }
>

到这里,常规的示例已经通过了,还留下一个 PartialByKeys<User, 'name' | 'unknown'> 在报错

因为他这里第二个字段传入的不一定在 User 里面,比如 unknown 就不在 User 的 key 中,而我们 K extends keyof T = keyof T 这个限制了参数的进来,所以只能把限制去掉,改成下面这样

type PartialByKeys2<T, K = keyof T> = Clone<
  {
    [P in keyof T as P extends K ? never : P]: T[P]
  } & {
    [P in K]?: T[P]
  }
>

去掉限制后,又新增了 2 个新的报错

  • K 的报错 Type ‘K’ is not assignable to type ‘string | number | symbol’.
  • T[P] 的报错 Type ‘P’ cannot be used to index type ‘T’.

因为确实没限制 k 的类型,而且 in 循环的则是 unio(联合类型)的变量
T[P] 因为 K 不一定就是 T 里面的键,T[‘unknown’] 肯定也是不存在的,所以报错了

所以当 K 循环的时候,还是用回刚才学的套路,在循环中判断键值 [P in K as P extends keyof T ? P : never]

T[P] 的问题解决就是先判断一下 P 是在 T 的范围内的才读 T[P] 就 OK 了

完整的正确答案如下:

type PartialByKeys2<T, K = keyof T> = Clone<
  {
    [P in keyof T as P extends K ? never : P]: T[P]
  } & {
    [P in K as P extends keyof T ? P : never]?: P extends keyof T ? T[P] : never
  }
>

最后

复盘一下几个问题

  • 如何在对象的循环中给键值做判断
    • as 关键字,as 后面接上条件语句,如果为 false,则返回 never 就可以排除掉某个键值了
  • P in keyof as any extends P ? never : P 是什么意思
    • P in keyof 是一组
    • extends P ? never : P 是一组
    • 最后 2 组的值用 as 链接起来,形成当前循环的键
  • 如果一定要用到 & 符,如何在返回之前合并 & 2 边的内容
    • & 符号是一个交叉合并的过程,合并出来的对象在约束数据的功能上没有区别
    • 可是交叉运算出来后的数据和单一类型用严格相等对比得到的结果是 false
    • 最后可以基于 Pick 工具,封装一个 Clone 工具,把 {} & {} 合并为一个 {}
<think>好的,我现在需要回答用户关于“类型体操”和TypeScript中是否存在类型体操的问题。首先,我需要明确这两个概念的定义以及它们之间的关系。 类型体操,这个词听起来有点像编程中的高级技巧,可能和类型系统有关。用户可能是在学习TypeScript时遇到了复杂的类型操作,所以想了解这个概念。我需要先定义类型体操,然后说明它在TypeScript中的存在和应用。 首先,类型体操的定义。根据我之前的了解,类型体操指的是在类型系统中进行复杂的类型操作和变换,类似于编程中的代码技巧,但这里专注于类型层面。可能需要举一些例子,比如条件类型、映射类型、泛型等高级特性。 然后,TypeScript中的类型体操是否存在。TypeScript具有强大的类型系统,支持泛型、条件类型、联合类型、交叉类型等,这些功能允许开发者进行复杂的类型操作。因此,TS中确实存在类型体操。需要具体举例,比如使用泛型约束、条件类型来创建工具类型,或者处理复杂的类型转换。 接下来,我需要组织回答的结构。用户希望结构清晰,逐步解决问题,所以应该分点说明。先介绍类型体操的概念,再讨论TS中的类型体操,包括其存在性和具体例子。同时,要注意使用中文,并遵循系统级指令中的格式要求,比如数学表达式用$...$,独立公式用$$...$$,但在这里可能更多是代码示例,所以需要正确使用代码块标记。 需要确保LaTeX语法正确,比如在行内使用$...$,独立公式单独成段。但在回答中,可能更多的代码示例,而不是数学公式,但如果有需要的话,比如解释类型理论中的某些概念,可能需要使用LaTeX。 然后,用户可能想知道为什么需要类型体操,以及它的应用场景。比如在开发库或框架时,为了提供更灵活的类型支持,或者确保类型安全,进行复杂的类型操作是必要的。例如,Redux中的Action类型、React组件Props的推断等都可能用到类型体操。 可能的例子包括TypeScript内置的工具类型,如Partial、Required、Pick、Record等,这些都是通过类型体操实现的。另外,条件类型如Exclude、Extract,以及infer关键字使用,比如在ReturnType中推断函数返回类型,都是类型体操的体现。 需要注意用户的背景,可能是开发者,对TS有一定了解,但可能不熟悉高级类型操作。因此,例子需要具体但不过于复杂,解释要清晰易懂。 检查是否有遗漏点,比如类型体操的优缺点,可能的好处是增强类型安全,提升代码质量,但缺点是可能增加复杂性,难以理解和维护。但用户没有问优缺点,所以可能不需要深入,但可以简要提及。 最后,确保回答符合格式要求,结构清晰,分点说明,使用中文,正确使用代码块和LaTeX格式。同时,验证信息的准确性,比如TypeScript的特性是否准确,例子是否正确。</think>### 分步解答:什么是类型体操TS中有类型体操吗? --- #### 一、类型体操的定义 **类型体操**(Type Gymnastics)是开发者利用编程语言的类型系统,通过复杂操作实现**类型推导、变换或约束**的技术。 - **核心目标**:在编译阶段验证数据结构的合法性,提升代码安全性。 - **类比**:类似于编程中通过算法解决问题,但操作对象是类型而非数据。 例如,在数学中构造满足条件的集合: $$ S = \{ x \in \mathbb{N} \mid x \mod 2 = 0 \} $$ 类型体操则是通过代码语法定义类似规则的“类型集合”。 --- #### 二、TypeScript中的类型体操 TypeScriptTS)具备强大的**静态类型系统**,支持以下特性实现类型体操: ##### 1. 基础工具 - **泛型**:动态生成类型 ```typescript type Wrap<T> = { value: T }; // 包裹任意类型T ``` - **联合类型**:组合多种类型 ```typescript type Status = "success" | "error"; ``` ##### 2. 进阶操作 - **条件类型**:基于逻辑判断生成类型 ```typescript type IsNumber<T> = T extends number ? "Yes" : "No"; ``` - **映射类型**:批量转换类型属性 ```typescript type Readonly<T> = { readonly [K in keyof T]: T[K] }; ``` - **模板字面量类型**:动态生成字符串类型 ```typescript type EventName = `on${"Click" | "Hover"}`; // "onClick" | "onHover" ``` ##### 3. 实战案例 **案例1:深度递归只读化** ```typescript type DeepReadonly<T> = { readonly [K in keyof T]: T[K] extends object ? DeepReadonly<T[K]> : T[K]; }; ``` **案例2:提取函数参数类型** ```typescript type Parameters<T> = T extends (...args: infer P) => any ? P : never; ``` --- #### 三、为何需要类型体操? 1. **精准约束**:如确保API参数符合特定格式。 2. **灵活推断**:自动推导复杂数据结构(如Redux的Action类型)。 3. **代码自文档化**:通过类型直接体现业务逻辑,降低维护成本。 --- #### 四、总结 - **TypeScript支持类型体操**:通过泛型、条件类型等实现复杂类型操作。 - **典型场景**:框架开发、工具库设计、业务模型抽象。 - **平衡建议**:适度使用以提升安全性,避免过度复杂化。 例如,用类型体操实现一个“安全路径访问”工具: ```typescript type Path<T, K extends string> = K extends keyof T ? K : K extends `${infer A}.${infer B}` ? (A extends keyof T ? `${A}.${Path<T[A], B>}` : never) : never; ```
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值