TS类型体操 之 中级类型体操挑战收官之战

本文详细解析了 TypeScript 中如何运用无中生有和泛型解决复杂类型挑战,包括理解 `infer` 关键字、`extends` 的应用以及如何在 `declare` 声明中处理参数和返回值。通过实例讲解了 `Promise.all` 和链式调用的类型定义,强调了在没有初始泛型时如何创造参数和利用类型推断。此外,文章还提醒读者注意 `readonly`、`as const` 等关键字在类型安全中的作用,并鼓励读者在遇到困难题时灵活运用这些技巧。

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

中级类型体操挑战收官之战

之前在写入门前置知识的时候就提到过 “有几道题目没解出来”,其实这 2 道题目和后面 “困难” 的部分题目题型很像
所以今天理解透这 2 道题目后中级类型应该就过关了,顺便还能为“困难”打下基础

分别是 : 20・Promise.all 12・可串联构造器

这 2 个题目我个人总结就是 “无中生有”,不存在的参数,就自己创建一个

热身 - 3196 · Flip Arguments

在讲上面 2 个题目时想先讲一下 3196・Flip Arguments ,一个 获取方法内参数,并且将他们反转 的一个题目(之前好像也没机会讲)

这个题目其实就是一个简单的 extends 的应用,难在一个小小的思维转变

需求如下:获取一个方法内的参数,并且将他们反转过来

type cases = [
  Expect<Equal<FlipArguments<() => boolean>, () => boolean>>,
  Expect<Equal<FlipArguments<(foo: string) => number>, (foo: string) => number>>,
  Expect<Equal<FlipArguments<(arg0: string, arg1: number, arg2: boolean) => void>, (arg0: boolean, arg1: number, arg2: string) => void>>,
]

在这个题目,需要理解几个点

  1. extends 其实就是一个包含关系,包括 方法 也可以 extends
  2. 说到反转内容,数组是最容易反转的,03192 就是 Reverse 的题目了
  3. 方法中的参数,基于 ES6 的知识,结合 ... 就能形成数组了,和 2 相呼应

所以解题重点就在于 extends

答案如下:

// Reverse 是 03192 题目已经做出来的,这里就直接复用不重复写了
type FlipArguments<T> = T extends (..._: infer args) => infer R ? (...arg0: Reverse<args>) => R : T
  • infer 是计算,是占位,之前有讲过, infer R 就是帮返回值占个位
  • ..._: infer args 这一块则是整个题目的灵魂所在,这里我们不能用 infer 占位,只能用一个变量来占(这个变量起什么名字都行),然后他的类型才是 infer args 这样的话,args 则代替了输入的参数的全部内容了
  • 包括最终返回的时候 (...arg0: Reverse<args>) 这个表达式中,args0并没有任何实际意义,也是一个参数占位而已

讲这个题目主要是为了 20・Promise.all 做个铺垫,因为 Promise.all 也有获取参数的场景

TS 类型体操遇上 declare

declare 是一个声明,可以看到 TS 的源码中对 JS 的方法很多都用了声明,这也是为什么我们使用 parseInt 能有类型检查的原因。declare就不在这里展开,姑且理解为 为一个方法/函数约定参数和返回值


讲题 : 00020-medium-promise-all

要求:键入函数 PromiseAll,它接受 PromiseLike 对象数组,返回值应为 Promise,其中 T 是解析的结果数组。

答案初始模版:

declare function PromiseAll(values: any): any

本以为只是一个简单的获取 values 然后包裹一层 Promise 的操作,可是由于各种语法问题,PromiseAll 本身就是一个方法而不是泛型参数,所以上面讲的套路行不通了(注意区分和对比)。

扣一下今天的主题无中生有

  • 在之前的题目中,比如我们要用到计数器,我们会新建一个 C extends unknown[] = []
  • 需要新的变量都会新建一个变量并且赋一个默认值去用,这也算无中生有

Q:思考一下在 declare 中如果想无中生有的话,要加那里?
A:方法想无中生有,加泛型!比如下面这样的

declare function PromiseAll<T>(values: any): any

在回到题目,返回值应为 Promise,其中 T 是解析的结果数组。

  • 返回值是 Promise
  • Promise 里面的泛型类型 T 是一个数组(这里的 T,也是无中生有生出来的,不然原题模版哪里来的 T),所以我们直接约束 T 为一个数组
declare function PromiseAll<T extends any[]>(values: any): Promise<T>

// 测试用例
type TestPromise = typeof promiseAllTest1 // 这时候得到的是 Promise<any[]>

到这一步就已经成功了一半,返回 Promise,并且泛型 T 是数组,剩下的一半就是把 T 转换为具体的数组,而不是 any[]

错误示范,错误示范,错误示范

// 报错
// 'T' is declared but its value is never read.
// 'infer' declarations are only permitted in the 'extends' clause of a conditional type.
declare function PromiseAll<T extends any[]>(values: infer T): Promise<T>

// 或 错误示范2:
// 不报错可是也没效果
declare function PromiseAll<T extends any[]>(values: T): Promise<T>
  • 错误 1 意思是 infer 只能在 extends 表达式里面去占位,普通情况下不行
  • 错误示范 2 中,因为 T 本来就是 any[] ,values 也确实是数组,没毛病,可是也推导不出来结果

正确 25% 的答案:

declare function PromiseAll<T extends any[]>(values: [...T]): Promise<T>

利用错误示范 2 中的原理,反推 T,values 是数组,而我们要做的是获取这个数组里面的内容, 如果我们把 T 分散了([...T])这个类型依旧没报错的话,T 就和 values 完全相等了,这时候返回 T,测试用例第一个例子就 pass 了

这时候测试用例是过了,可是 3,4 行代码类型检查不过。
as const 这个也在 前置知识里面提到过,as const 的会把所有的值拿出来,而且变成 readonly。因为 const 确实是只读的标记。

正确 50% 的答案:

declare function PromiseAll<T extends any[]>(values: readonly [...T]): Promise<T>

加上 readonly 后,声明的等式成立了,就还差 2 个测试用例的情况,因为他们传入的数值里面包含 Promise.resolve 这种情况。我们需要从 Promise.resolve 中把参数取出来,那么就要从返回值 Promise<T> 去入手了

Promise<T> 注意这里的 <T>已经是 泛型 了,又回到熟悉的 type 体操的感觉了,这时候题目就变成了

正确 100% 的答案(复杂版):

declare function PromiseAll<T extends any[]>(values: readonly [...T]): Promise<PromiseRes<T>>

type PromiseRes<T extends any[], R extends any[] = []> = T extends [infer F, ...infer Rest] ? PromiseRes<Rest, [...R, F extends Promise<infer A> ? A : F]> : R

新建了一个 PromiseRes 为了处理 T 这个数组,F extends Promise<infer A> ? A : F 就是为了判断是不是 Promise 类型的,是的话把 A 提出来,最后存到一个数组里面返回。用例通过,木的问题


正确 100% 的答案(简单版):

declare function PromiseAll<T extends any[]>(values: readonly [...T]): Promise<{ [P in keyof T]: T[P] extends Promise<infer R> ? R : T[P] }>

这里有 2 个小知识点稍微在拓展下

Promise<{[P in keyof]}> 里面为什么可以这样写

说再多还不如写段代码

// 写法1.
setTimeout(function () {
  console.log('定时器')
}, 1000)

// 写法2
var log = function () {
  console.log('定时器')
}
setTimeout(log, 1000)

2 种写法最后运行效果一模一样,同理,上面简单版写法还能写成这样的:

declare function PromiseAll<T extends any[]>(values: readonly [...T]): Promise<PromiseRes<T>>

type PromiseRes<T> = { [P in keyof T]: T[P] extends Promise<infer R> ? R : T[P] }

{[P in keyof T]} 这不是对象的写法吗,为什么在这里最终会返回数组类型?

从 Pick 方法开始学体操的时候 {[P in keyof T]}确实返回的都是对象类型,毕竟 {} 在那里摆着

不过凡事都有例外,因为 T 是个数组类型,而 keyof T 得到的其实是 0,1,2,3… 数组长度的索引,和数组特有的属性方法。所以 P 对应的也是 0,1,2,3… + 特有的属性和方法

比如这个例子中

var aKeys:ArrKeys = ''
type Arr = ['Jioho', 'Promise']
type ArrKeys = keyof Arr

根据智能提示看到 ArrKeys 的取值范围全是数组的方法和属性

所以说,{[P in keyof T]} 的返回值还得看 T 到底是什么类型

12 · 可串联构造器

这一题的难度在用这是一个链式调用,而且还得把之前的记录动态累计下来

const result1 = a.option('foo', 123).option('bar', { value: 'Hello World' }).option('name', 'type-challenges').get()

之前接触的题目都是传入数据,得出结果。而且这个题目和 Promise.all 一样,没有给出很多的初始泛型,这就又得靠我们自己的 无中生有 技巧

解题的思路上面也有说了

  • 一个是调用 option 时 键值对 动态累计下来
  • 题目给给出一个 get 来获取结果

突破点首先在 get,因为这才是获取结果的位置,假设我们返回 T,T 包含了所有的键值对内容。

按作用域来看,T 肯定是作为累计的变量,所以 T 作用域应该在 getoption 之上,第一步的无中生有就给 Chainable 加一个泛型,然后记得补上默认值(根据返回结果,返回的应该是个对象)

第一步思考结果如下:

type Chainable<T = {}> = {
  option(key: string, value: any): any
  get(): T
}

第二步:链式调用和变量累计?

链式调用其实核心原理就是把 this 把当前对象作为调用的返回值。

Chainable(a)含有 option 方法,调用 option 后在返回一个 Chainable(a),这时候返回值就又能继续调用了

做过前面题目的其实都应该知道,想做到变量累积,那必须是递归(比如计数器的累积),不断的调用自身,并且不断追加新参数进去

结合上面说的 2 点:递归,返回自己,追加参数,得出下面的答案

这是有点错误的示范:

// 这是 527 题目的答案,为Object追加新的键值对。复用上了
type AppendToObject<T, U extends string, V> = { [P in keyof T | U]: P extends keyof T ? T[P] : V }

type Chainable<T = {}> = {
  // 错误地方: AppendToObject<T,key, value>
  option(key: string, value: any): Chainable<AppendToObject<T,key, value>>
  get(): T
}

以上的结果思路是对的,不过就是注释的地方有点问题 AppendToObject<T,key, value> 这时候的 key 和 value 还是 JS 变量,而不是 TS 的内容。

这时候可能就会想到 typeof 不是可以把 JS 的内容转换为 TS 吗?

转是可以转,不过转了之后返回值是 stirng,而不是我们想要的 123

var b = '123'
type Test = typeof b // string

到这一步,应该要想起 Promise.all 题目讲到的 变量倒推,复习下 Promise.all

declare function PromiseAll<T extends any[]>(values: readonly [...T]): ...

定义了泛型 T,直接 readonly [...T] 放入 values 中进行一个类似倒推的操作。对于链式调用来说同理

type AppendToObject<T, U extends string, V> = { [P in keyof T | U]: P extends keyof T ? T[P] : V }

type Chainable<T = {}> = {
  option<K extends string,V>(key: K, value: V): Chainable<AppendToObject<T,K,V>>
  get(): T
}

定义KV,这 2 个类型放入参数中,进行一个倒推,用 K 来代表 key 的值,V 代表 value 的值,在结合 AppendToObject,就可以为 T 动态添加参数了

与此同时 option 的返回值返回的则是最新的 Chainable 和当前链式调用后最全的 T,这时候在调用 get 就能把累积的变量都拿出来了

总结

这几个题目给了非常好的提示性效果,没有参数要学会自己创造参数,学会无中生有

如果能用 infer 就尽量用好 infer,不过 infer 只能在 extends 相关的表达式里面去用

像 Promise.all 和链式调用这种函数/方法里面用不了的,就用一个新变量进行一个类型的倒推,把对应的值反过来约束为对应的泛型

尤其是最后一题,一定要好好理解,因为我去探过路了,困难题 中会遇到很多这样的情况,需要学会 无中生有和并对象

至此中等题目就刷完了,我觉得比较有用的技巧也总结了好几篇笔记,欢迎到我的 TS 专栏翻一翻,点个赞支持下。祝你们也刷题愉快,困难题见!

<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; ```
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值