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();
问题分析
原代码的核心问题在于:
- 泛型参数
SeriesKeysRemainingT没有被正确初始化和传递,导致TypeScript无法跟踪剩余的系列键; AddLineReturn中NL extends Exclude<RemaingAfterLabelRemoved, NL>的约束是循环引用,完全无效;- 剩余系列的类型收窄逻辑错误,无法正确关联运行时剩余对象和类型层面的剩余键。
修正后的实现方案
我们可以通过明确区分构建器的两种状态(有剩余系列/无剩余系列),并利用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');
实现说明
- 状态区分:通过
ChartBuilderWithRemaining和ChartBuilderReadyToBuild两个类型明确区分构建器的不同状态,TypeScript会根据剩余键的存在与否自动切换类型; - 泛型推导:利用
keyof SD获取初始系列的所有键,每次调用addLine后用Exclude<RemainingKeys, K>移除已添加的键,当剩余键为空时自动切换到ChartBuilderReadyToBuild类型; - 运行时同步:用
Set跟踪运行时剩余的键,确保类型层面的剩余键和实际运行时状态一致。
这样实现后,构建器会严格限制只能添加未使用过的系列键,且所有系列添加完成后,addLine方法会消失,只能调用build(),完全满足类型安全的需求。
内容的提问来源于stack exchange,提问作者user3241778
相关产品推荐
相关产品推荐

