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

如何用TypeScript递归类型实现对象字段的递进必选约束?

TypeScript递进约束类型实现问题

我定义了如下TypeScript类型:

type WeakGuaranteeType = {
  a?: string,
  b?: string,
  c?: string,
  d?: string,
}

我希望实现更强的类型约束:当b存在时a必须存在,当c存在时b必须存在,以此类推。即:

{ a: "" }               // 合法
{ a: "", b: "" }        // 合法
{ a: "", b: "", c: "" } // 合法
{ c: "" }               // 不合法!!

是否可以用TypeScript描述这种类型?

我的首次尝试如下:

type BetterType = 
  | {}
  | ({a: string} & (
      | {}
      | ({b: string} & (
          | {}
          | ({c: string} & (
              // 依此类推...
            ))
        ))
    ))

该方案存在两个问题:

  • 写法冗长,需手动编写;
  • 会触发“Expression produces a union type that is too complex to represent”错误。

我认为递归方案更合适,于是编写了如下代码:

type RecursiveRequire<Keys extends string[]>
  = Keys extends []  // 检查Keys是否为空
    ? {}             // 基准情况:若Keys为空,返回{}
    : (              // 递归情况:添加第一个键并对剩余键递归
        { [Key in Keys[0]]: string } &
        RecursiveRequire</* 此处应如何表示“Keys中除第一个外的所有元素”? */>
      );

type BestType = RecursiveRequire<["a", "b", "c", "d"]>;

请问如何完善这个递归类型实现?


解决方案

首先,解决“Keys中除第一个外的所有元素”的问题,可以用TypeScript的数组解构推断:Keys extends [infer First extends string, ...infer Rest extends string[]],其中Rest就是除第一个元素外的剩余元素数组。

同时需要调整递归逻辑——原写法会生成所有键都必填的交叉类型,而我们需要的是“递进可选的联合类型”(允许停在任意步骤,但前面的键必须存在)。正确的递归实现如下:

type RecursiveRequire<Keys extends string[]> = 
  // 基准情况:空数组返回空对象
  Keys extends [] 
    ? {} 
    // 解构数组,提取第一个元素First和剩余元素Rest
    : Keys extends [infer First extends string, ...infer Rest extends string[]]
      ? 
        // 两种情况的联合:
        // 1. 空对象(允许不选任何键)
        {} | 
        // 2. 必须包含当前First键,同时递归约束剩余键的递进规则
        ({ [K in First]: string } & RecursiveRequire<Rest>)
      : never;

// 测试类型
type BestType = RecursiveRequire<["a", "b", "c", "d"]>;

// 合法示例
const valid1: BestType = {};
const valid2: BestType = { a: "" };
const valid3: BestType = { a: "", b: "" };
const valid4: BestType = { a: "", b: "", c: "" };
const valid5: BestType = { a: "", b: "", c: "", d: "" };

// 不合法示例(TypeScript会报错)
const invalid1: BestType = { b: "" }; // 缺少a,报错
const invalid2: BestType = { c: "" }; // 缺少a和b,报错
const invalid3: BestType = { a: "", c: "" }; // 缺少b,报错

核心逻辑说明

  • 每一层递归都会生成两种可选类型:要么不选当前层级及以后的所有键(空对象),要么必须选当前键,同时遵守剩余键的递进约束。
  • 通过递归展开,最终会生成所有符合要求的递进类型联合:{} | {a:string} | {a:string,b:string} | {a:string,b:string,c:string} | {a:string,b:string,c:string,d:string}

可选调整:排除空对象情况

如果你想只允许至少包含一个键的合法组合,可以去掉联合中的{},修改为:

type RecursiveRequireNonEmpty<Keys extends string[]> = 
  Keys extends [] 
    ? never 
    : Keys extends [infer First extends string, ...infer Rest extends string[]]
      ? 
        ({ [K in First]: string } & ({} | RecursiveRequireNonEmpty<Rest>))
      : never;

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.03 05:21:56