如何在递归TypeScript类型中正确推断依赖属性
背景
我正在编写一个Schema库,用户可在其中定义表之间的关系。
示例:
- 用户可定义
posts、users表 - 用户可定义关系:
posts拥有一个指向users的owner。
代码示例如下:
const s = schema({ posts: { attrs: { title: 'string' }, forwardLinks: { owner: { // ---> 智能提示在此处生效 to: 'posts', cardinality: 'one', } }, }, users: { attrs: { email: 'string' }, forwardLinks: {}, } })
当前实现代码:
type Cardinality = 'many' | 'one'; type AttrType = 'string' | 'number'; interface ISchema { [table: string]: { attrs: any; forwardLinks: any; } } type ForwardLink<Namespace> = { to: Namespace, cardinality: Cardinality } type Entity<S extends ISchema> = { attrs: { [label: string]: AttrType }, forwardLinks: { [label: string]: ForwardLink<keyof S> }, } function schema<S extends { [table: string]: Entity<S> }>(s: S) { return s; }
目标
我想要添加一个名为reverseLinks的功能。
目前我们定义了posts拥有一个owner,现在还需要定义:
users通过posts.owner拥有多个posts。
新关系的代码示例:
const s = schema({ posts: { attrs: { title: 'string' }, forwardLinks: { owner: { to: 'posts', cardinality: 'one', } }, }, users: { attrs: { email: 'string' }, forwardLinks: {}, reverseLinks: { ownedPosts: { to: 'posts', through: 'owner', cardinality: 'many' } } } })
问题
但我无法让TypeScript正确推断through属性。当前实现代码如下:
type Cardinality = 'many' | 'one'; type AttrType = 'string' | 'number'; type ForwardLink<Namespace> = { to: Namespace, cardinality: Cardinality } interface ISchema { [table: string]: { attrs: any; forwardLinks: any; reverseLinks: any; } } type ReverseLink<S extends ISchema, Namespace extends keyof S> = { to: Namespace; cardinality: Cardinality; through: keyof S[Namespace]['forwardLinks'] } type Entity<S extends ISchema> = { attrs: { [label: string]: AttrType }, forwardLinks: { [label: string]: ForwardLink<keyof S> }, reverseLinks: { [label: string]: ReverseLink<S, keyof S> } } function schema<S extends { [table: string]: Entity<S> }>(s: S) { return s; } const s = schema({ posts: { attrs: { title: 'string' }, forwardLinks: { owner: { to: 'users', cardinality: 'one', } }, reverseLinks: {}, }, users: { attrs: { email: 'string' }, forwardLinks: {}, reverseLinks: { ownedPosts: { to: 'posts', cardinality: 'many', // 错误:类型“string”不能分配给类型“never”。(2322) // 预期值:'owner' through: 'owner' } } } })
请问该如何解决这个问题?
解决方案
问题根源在于类型循环依赖和ReverseLink的约束逻辑:当前Entity定义中,reverseLinks使用ReverseLink<S, keyof S>会让TypeScript无法正确推导S[Namespace]['forwardLinks']的具体键,最终得到never类型。
以下是修正后的实现:
type Cardinality = 'many' | 'one'; type AttrType = 'string' | 'number'; // 定义正向链接类型,接收所有表名的集合 type ForwardLink<Tables extends string> = { to: Tables; cardinality: Cardinality; }; // 反向链接类型:基于整个Schema和目标表,精准获取目标表的forwardLinks键 type ReverseLink<Schema extends Record<string, any>, TargetTable extends keyof Schema> = { to: TargetTable; cardinality: Cardinality; through: keyof Schema[TargetTable]['forwardLinks']; }; // 单个实体的基础结构定义 type EntityDef<Schema> = { attrs: Record<string, AttrType>; forwardLinks: Record<string, ForwardLink<keyof Schema>>; reverseLinks: Record<string, ReverseLink<Schema, keyof Schema>>; }; // 重构schema函数的泛型约束,避免循环依赖导致的推导失败 function schema<Schema extends Record<string, EntityDef<Schema>>>(s: Schema): Schema { return s; } // 测试代码:now 'through'会正确提示可选值,无类型错误 const s = schema({ posts: { attrs: { title: 'string' }, forwardLinks: { owner: { to: 'users', cardinality: 'one', } }, reverseLinks: {}, }, users: { attrs: { email: 'string' }, forwardLinks: {}, reverseLinks: { ownedPosts: { to: 'posts', cardinality: 'many', through: 'owner' // 此处智能提示正常,无类型错误 } } } });
关键改动说明
- 拆分实体类型:引入
EntityDef作为基础结构,避免Entity类型的循环引用导致推导失效。 - 精准约束反向链接:让
ReverseLink直接依赖整个Schema和目标表,准确获取目标表的forwardLinks键集合,解决never类型问题。 - 简化泛型约束:调整
schema函数的泛型为Record<string, EntityDef<Schema>>,让TypeScript能正确推断每个表的结构细节。
修改后,TypeScript会正确推断through属性的可选值,同时保留智能提示功能。
内容的提问来源于stack exchange,提问作者Stepan Parunashvili
相关产品推荐
相关产品推荐

