You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.08 23:50:34