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

TypeScript泛型条件递归类型实现类型安全图表构建器遇阻求助

类型安全图表构建器实现方案

我想要创建一个类型安全的图表构建器,目标是用系列定义初始化构建器后,逐步添加线条;当所有线条都添加完成时,构建器自动提供build()方法,而非继续显示addLine()方法。目前实现遇到了困难,求TypeScript开发者帮忙解决。

原代码如下:

const seriesDef = {
  'Turnover': {
    unit: '€'
  },
  'Turnover Compare': {
    unit: '€'
  },
  'Turnover Diff': {
    unit: '€'
  }
};

type SeriesDef = {
  [seriesName: string]: {unit: string}
}


type AddLineReturn<RemaingAfterLabelRemoved> = {
 addLine<NL extends Exclude<RemaingAfterLabelRemoved, NL>>(nextLabel: NL): 
 RemaingAfterLabelRemoved extends undefined? {build(): void}: 
 AddLineReturn<Exclude<RemaingAfterLabelRemoved, NL>>
}

function testTypeNarrow<const SeriesKeysRemainingT, SD extends SeriesDef, L extends keyof SD>(remainingSeriesDef: SD, label: L): SeriesKeysRemainingT extends undefined? {build(): void}: AddLineReturn<Exclude<SeriesKeysRemainingT, L>>{
  let remainingSeriesDefAfterLabelRemoved = {...remainingSeriesDef} ;
  delete remainingSeriesDefAfterLabelRemoved[label]
  const typedRemainingSeriesDef = remainingSeriesDef as Omit<SD, L>;
  type RemaingAfterLabelRemoved = Exclude<SeriesKeysRemainingT, L> & keyof typeof typedRemainingSeriesDef;


  const addLineReturnObj : AddLineReturn<Exclude<SeriesKeysRemainingT, L>> = {
    addLine<NL extends RemaingAfterLabelRemoved>(nextLabel: NL){
    return testTypeNarrow<RemaingAfterLabelRemoved, typeof typedRemainingSeriesDef, NL>(typedRemainingSeriesDef, nextLabel)
  }
}

if(remainingSeriesDef === undefined){
  return {
    build(){
      console.log("BUILDING")
    }
  }
}else{
  return addLineReturnObj;
 }
}

const narrowedWithoutTurnover = testTypeNarrow(seriesDef, 'Turnover')
narrowedWithoutTurnover
.addLine('Turnover Compare')
.addLine('Turnover Diff')
.build();

问题分析

原代码的核心问题在于:

  1. 泛型参数SeriesKeysRemainingT没有被正确初始化和传递,导致TypeScript无法跟踪剩余的系列键;
  2. AddLineReturn中NL extends Exclude<RemaingAfterLabelRemoved, NL>的约束是循环引用,完全无效;
  3. 剩余系列的类型收窄逻辑错误,无法正确关联运行时剩余对象和类型层面的剩余键。

修正后的实现方案

我们可以通过明确区分构建器的两种状态(有剩余系列/无剩余系列),并利用TypeScript的Omit和泛型推导来实现类型安全的逐步收窄:

const seriesDef = {
  'Turnover': { unit: '€' },
  'Turnover Compare': { unit: '€' },
  'Turnover Diff': { unit: '€' }
};

type SeriesDef = Record<string, { unit: string }>;

// 构建器状态1:还有剩余系列可添加
type ChartBuilderWithRemaining<RemainingKeys extends string> = {
  addLine<K extends RemainingKeys>(key: K): 
    Exclude<RemainingKeys, K> extends never 
      ? ChartBuilderReadyToBuild 
      : ChartBuilderWithRemaining<Exclude<RemainingKeys, K>>;
};

// 构建器状态2:所有系列已添加,可执行build
type ChartBuilderReadyToBuild = {
  build(): void;
};

// 创建构建器的工厂函数
function createChartBuilder<SD extends SeriesDef>(seriesDef: SD): 
  keyof SD extends never 
    ? ChartBuilderReadyToBuild 
    : ChartBuilderWithRemaining<keyof SD & string> {
  const remainingKeys = new Set(Object.keys(seriesDef));

  const builder: any = {
    addLine(key: string) {
      remainingKeys.delete(key);
      return remainingKeys.size === 0 ? readyBuilder : builder;
    }
  };

  const readyBuilder: ChartBuilderReadyToBuild = {
    build() {
      console.log("BUILDING CHART");
    }
  };

  return remainingKeys.size === 0 ? readyBuilder : builder;
}

// 测试
const builder = createChartBuilder(seriesDef);
builder
  .addLine('Turnover')
  .addLine('Turnover Compare')
  .addLine('Turnover Diff')
  .build(); // 此时addLine已不可用,只能调用build

// 错误示例:添加重复键会触发类型错误
// builder.addLine('Turnover').addLine('Turnover');

实现说明

  1. 状态区分:通过ChartBuilderWithRemaining和ChartBuilderReadyToBuild两个类型明确区分构建器的不同状态,TypeScript会根据剩余键的存在与否自动切换类型;
  2. 泛型推导:利用keyof SD获取初始系列的所有键,每次调用addLine后用Exclude<RemainingKeys, K>移除已添加的键,当剩余键为空时自动切换到ChartBuilderReadyToBuild类型;
  3. 运行时同步:用Set跟踪运行时剩余的键,确保类型层面的剩余键和实际运行时状态一致。

这样实现后,构建器会严格限制只能添加未使用过的系列键,且所有系列添加完成后,addLine方法会消失,只能调用build(),完全满足类型安全的需求。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.09 06:30:09