本文探讨在 TypeScript 中如何从深层嵌套的接口中提取类型。针对可选属性导致的联合类型问题,介绍了使用内置 Required 类型的简单方案,并详细解析了利用映射类型和条件类型实现递归去除可选修饰的高级类型编程技巧,包括处理 TypeScript 懒计算机制的触发方法。
前言
近日,开发者东东在 TypeScript 开发中遇到一个类型处理难题,特来请教。
具体场景如下:
定义了一个包含可选属性的接口,目标是提取其中 userInfo 的类型信息:
interface Result{
data?: {
userInfo?: {
name: string;
}
}
}他尝试通过索引访问的方式直接获取:
type userInfo = Result['data']['userInfo'];然而编译器报错:

错误提示指出 userInfo 不在预期的联合类型范围内。
这一结果符合预期,因为可选索引类型的本质是值类型与 undefined 的联合,即 value | undefined。
因此,他询问正确的提取方式。
我向他说明,解决此问题存在两种复杂度不同的方案,一简一繁,询问他偏好哪一种。
他选择了简单的方案,代码如下:
type userInfo = Required<Required<Result>['data']>['userInfo']这里利用了 TypeScript 内置的高级类型 Required,其作用是将索引类型中的所有可选修饰符移除。

因此,只需对每一层嵌套应用 Required 处理后,再提取索引值即可。
尽管该方案简洁,但当嵌套层级较深时,需要重复书写多次 Required,操作较为繁琐。
随后,东东询问了复杂方案的实现方式。
复杂方案虽然定义过程较为冗长,但优势在于使用便捷,无论嵌套多少层,只需定义一次:
首先,我们需要理解 Required 的内部实现原理:

该实现运用了映射类型语法,旨在对索引类型进行变换,从而生成新的索引类型。
其中,P in keyof T 表示遍历索引类型 T 中的所有键 P,用于构造新索引类型,其值保持为 T[P] 不变。
在构造过程中,可以添加或移除可选修饰符,也可以对值和索引进行其他修改。
与 Required 相对的 Partial 类型,其实现逻辑如下:

若希望一次性处理所有层级并移除所有可选修饰,则需采用递归处理方式,具体实现如下:
type DeepRequired<Obj> = {
[Key in keyof Obj]-?: IsOptional<Key, Obj> extends never? Obj[Key]: DeepRequired<Obj[Key]>
}该逻辑遍历索引类型 Obj 中的所有键 Key,通过 -? 操作符移除可选修饰,并对值进行判断:若值仍为可选索引类型,则递归处理。
接下来,如何定义一个名为 IsOptional 的高级类型,以判断索引是否为可选类型?
判断类型的依据在于其性质,可选类型的性质表现为 value | undefined,即值可能为空。
可选性判断的具体实现如下:
type IsOptional<Key extends keyof Obj, Obj> =
{} extends Pick<Obj, Key> ? Key : never;Obj 为索引类型,Key 为其某个键。由于可选索引的性质允许为空,因此 {} 可能是该索引类型的子类型。
此处使用的 Pick 同样是 TypeScript 内置的高级类型,用于选取部分索引并构造新的索引类型:

其实现同样依赖于映射类型语法:

当 Key 不存在时,Pick<Obj, Key> 的结果即为 {}。因此,可以通过判断 {} extends Pick<Obj, Key> 是否成立,来确定 Key 是否为可选索引。
综上所述,递归去除索引类型可选修饰的实现如下:
type IsOptional<Key extends keyof Obj, Obj> =
{} extends Pick<Obj, Key> ? Key : never;
type DeepRequired<Obj> = {
[Key in keyof Obj]-?: IsOptional<Key, Obj> extends never? Obj[Key]: DeepRequired<Obj[Key]>
}进行代码测试:

现在只需定义一次,即可提取任意层级的索引值,大大提升了便利性。
虽然上述写法已可直接使用,但在某些情况下可能会遇到类型未完全展开的问题:

TypeScript 并未直接计算出最终的结果类型。
这是 TypeScript 的默认机制,即懒计算(Lazy Evaluation),未使用的类型不会参与计算。
如何强制 TypeScript 计算出最终结果?
只需添加一段触发计算的逻辑,例如 xxx extends any 这种必然成立的条件判断:

再次测试,即可观察到 TypeScript 计算出了最终的结果类型:

总结
若要提取可选索引的值,需先使用 Required 移除可选修饰,再提取索引。当层级较多时,逐层处理较为繁琐,可采用类型编程进行递归处理。
利用映射类型语法移除索引类型的可选修饰,并判断值的类型;若值仍为可选索引,则继续递归处理。
判断可选索引的依据是其性质:可选索引的值为 value | undefined。若 {} extends Pick<Obj, Key> 成立,则表明该 Key 为可选索引。
可能会遇到类型未完全计算的情况,这是 TypeScript 默认懒计算的机制。可通过添加 xx extends any 这类不影响结果的条件类型来触发计算。
逐层使用 Required 处理在层级较少时较为简单,但层级较多时递归处理更为便捷。此外,此类高级类型具备可复用性,可应用于其他场景,这正是类型编程的优势所在。
到此关于 TypeScript 类型编程中索引类型递归去除可选修饰的介绍结束,更多相关 TypeScript 索引类型递归内容请搜索本站以前的文章或继续浏览下面的相关文章,希望大家以后多多支持本站!