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

如何在递归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' // 此处智能提示正常,无类型错误
      }
    }
  }
});

关键改动说明

  1. 拆分实体类型:引入EntityDef作为基础结构,避免Entity类型的循环引用导致推导失效。
  2. 精准约束反向链接:让ReverseLink直接依赖整个Schema和目标表,准确获取目标表的forwardLinks键集合,解决never类型问题。
  3. 简化泛型约束:调整schema函数的泛型为Record<string, EntityDef<Schema>>,让TypeScript能正确推断每个表的结构细节。

修改后,TypeScript会正确推断through属性的可选值,同时保留智能提示功能。

内容的提问来源于stack exchange,提问作者Stepan Parunashvili

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.16 04:02:33