TypeScript递归类型定义使用疑问:如何实现最大兼容类型校验
实现TypeScript递归联合类型的“最大接受型”
问题描述
我在TypeScript 3.7文档中看到以下递归类型示例:
type Json = | string | number | boolean | null | { [property: string]: Json } | Json[];
声明返回该类型的解析函数后:
declare var MyJSON: { parse: (s: string) => Json; };
对比返回any的原生JSON.parse,下面的代码能正常运行:
const j1 = JSON.parse('{foo: 123}'); console.log(j1.foo);
但使用MyJSON.parse时会触发报错:
const j2 = MyJSON.parse('{foo: 123}'); console.log(j2.foo);
先是'j2' is possibly 'null'.(18047),添加非空断言后又出现Property 'foo' does not exist on type 'string | number | boolean | { [property: string]: Json; } | Json[]'.(2339)。
我明白这是因为foo仅存在于联合类型的对象分支中。现在需要一种类型,既允许console.log(j2.foo)这类访问不报错,又保留类型校验(比如给j2.foo赋值new Image()时必须报错,因为Image不属于Json类型)。
实际场景中我有一个更受限的递归类型,遇到了同样的问题:
type DataCollection = Float32Array | { [property: string]: DataCollection } | DataCollection[];
我清楚返回实例的属性,但访问时会触发大量TS警告,不想用any或随处写类型断言,希望找到折中方案。
理想的“最大接受型”行为如下:
const d = getSomeDataCollection(); d.set(...); // 允许(d可能是Float32Array) d[0].set(...); // 允许(d可能是Float32Array数组) d.foo.set(...); // 允许(d可能是对象类型) d.min(...); // 报错(所有分支都无min方法) d == "hello"; // 报错(无分支可与string比较)
这种类型可以类比:
- 最小接受型(接口):严格限制存在的属性
interface PersonI { name: string; country: string; } const p = getPersonI(); console.log(p.nickname.length); // 报错(不存在nickname) - 最大接受型(Record):允许访问任意可能的属性
type PersonR = Record<string, string> const p = getPersonR(); console.log(p.nickname.length); // 允许(即使nickname可能不存在)
解决方案:交叉类型+递归条件类型
你可以通过交叉类型给每个基础类型添加索引签名和数组特性,构建出“最大接受型”的递归类型,既允许合法的属性访问,又保留类型约束。
通用封装工具类型
先定义一个通用工具类型,给任意类型添加宽松的递归特性:
type LooseRecursive<T> = T & { [key: string]: LooseRecursive<T>; } & LooseRecursive<T>[];
改造Json类型
用工具类型生成JsonMax:
type JsonMax = LooseRecursive<string | number | boolean | null>;
测试使用:
declare var MyJSON: { parse: (s: string) => JsonMax; }; const j2 = MyJSON.parse('{foo: 123}'); console.log(j2.foo); // 不再报错 j2.foo = new Image(); // 报错(Image不属于JsonMax类型)
改造DataCollection类型
同样生成DataCollectionMax:
type DataCollectionMax = LooseRecursive<Float32Array>;
测试理想场景:
declare function getSomeDataCollection(): DataCollectionMax; const d = getSomeDataCollection(); d.set(...); // 允许 d[0].set(...); // 允许 d.foo.set(...); // 允许 d.min(...); // 报错(无此方法) d == "hello"; // 报错(类型不匹配) d.foo = new Image(); // 报错(类型不匹配)
原理说明
- 交叉类型
T & {[key:string]: ...} & ...[]让TypeScript认为每个基础类型都同时具备对象索引签名和数组特性,因此访问任意属性或数组下标都不会触发类型错误; - 赋值时,TypeScript会校验值是否符合
LooseRecursive<T>的约束,非法类型会被拦截; - 完全避免了
any的类型逃逸,也无需频繁使用类型断言。
注意事项
- 这种类型允许访问不存在的属性,运行时建议配合可选链
?.处理属性不存在的情况; - 如果需要精确的类型分支判断,仍需结合类型守卫缩小类型范围,但日常开发中该方案可大幅减少冗余代码。
内容的提问来源于stack exchange,提问作者samanthaj
相关产品推荐
相关产品推荐

