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

如何用可变元组类型递归转换TypeScript元组元素?

问题描述

我有一个包含不同类型元素的TypeScript元组,想要将其转换为长度相同、元素类型基于输入元组的新元组。元组中的每个元素都是Extractor类的实例,该类用于从JSON结构中提取指定名称的特定类型列。我需要把存在对应列名的提取器转换为实现了index getter的子类实例,不存在对应列名的则保持原样。

我知道TypeScript里用Array.map没法在不全局修改定义的情况下转换元组元素,所以尝试用接收可变元组类型作为参数的递归函数实现,但代码编译失败。请问这个需求是否可行?如果可行,该怎么实现?


类型定义

abstract class Extractor<T extends Extractor<T, S>, S> {
  constructor(public name: string) { }

  abstract withIndex(index: number): T
  abstract get index(): number
  abstract get value(): S
}

class NumberExtractor extends Extractor<NumberExtractor, number> {
  override withIndex(index: number): NumberExtractorWithIndex {
    return new NumberExtractorWithIndex(this.name, index)
  }

  override get index(): number {
    throw new Error("No index")
  }

  override get value(): number {
    throw new Error("No value")
  }
}

class NumberExtractorWithIndex extends NumberExtractor {
  constructor(name: any, private _index: number) {
    super(name)
  }

  override get index(): number {
    return this._index
  }
}

class StringExtractor extends Extractor<StringExtractor, string> {
  override withIndex(index: number): StringExtractorWithIndex {
    return new StringExtractorWithIndex(this.name, index)
  }

  override get index(): number {
    throw new Error("No index")
  }

  override get value(): string {
    throw new Error("No value")
  }
}

class StringExtractorWithIndex extends StringExtractor {
  constructor(name: any, private _index: number) {
    super(name)
  }

  override get index(): number {
    return this._index
  }
}

使用any的函数实现(可运行但类型不安全)

const columns = ["A", "B", "C"]

function fnAny(...args: Extractor<any, any>[]): Extractor<any, any>[] {
  let [first, ...rest] = args

  for (const [index, column] of columns.entries()) {
    if (first.name === column) {
      first = first.withIndex(index);
      break;
    }
  }

  return args.length === 1
    ? [first]
    : [first, ...fnAny(...rest)]
}

const input = [new StringExtractor("A"), new NumberExtractor("X"), new NumberExtractor("C")] as const

console.log("input")
console.log(input)

const outputAny = fnAny(...input)

console.log("outputAny")
console.log(outputAny)

使用可变元组类型的函数(编译失败)

function fn<U extends Extractor<any, any>, V extends Extractor<any, any>[]>(...args: [U, ...V]): [U, ...V] {
  let [first, ...rest] = args

  for (const [index, column] of columns.entries()) {
    if (first.name === column) {
      first = first.withIndex(index);
    }
  }

  return args.length === 1
    ? [first]
    : [first, ...fn(...rest)]
}

const output = fn(...input)
console.log("output")
console.log(output)

解决方案

这个需求完全可行,核心是通过条件类型映射元组元素,并让递归函数正确推导转换后的类型。

1. 定义类型映射工具

首先需要一个类型,用来判断某个Extractor实例是否需要转换为带索引的子类:

type MappedExtractor<E extends Extractor<any, any>> = 
  E extends StringExtractor ? (E["name"] extends typeof columns[number] ? StringExtractorWithIndex : StringExtractor) :
  E extends NumberExtractor ? (E["name"] extends typeof columns[number] ? NumberExtractorWithIndex : NumberExtractor) :
  E;

// 扩展到元组的映射类型
type MappedExtractorTuple<T extends readonly Extractor<any, any>[]> = {
  [K in keyof T]: MappedExtractor<T[K]>;
};

2. 实现类型安全的递归函数

修改递归函数的泛型定义,让它返回映射后的元组类型,同时在函数内部处理类型断言(因为TypeScript无法自动推导withIndex的返回类型与原类型的替换关系):

const columns = ["A", "B", "C"] as const;

function mapExtractors<T extends readonly Extractor<any, any>[]>(
  ...args: T
): MappedExtractorTuple<T> {
  if (args.length === 0) return [] as MappedExtractorTuple<T>;

  const [first, ...rest] = args;
  let processedFirst = first;

  const columnIndex = columns.indexOf(first.name as typeof columns[number]);
  if (columnIndex !== -1) {
    processedFirst = first.withIndex(columnIndex) as MappedExtractor<typeof first>;
  }

  return [processedFirst, ...mapExtractors(...rest)] as MappedExtractorTuple<T>;
}

3. 使用示例

const input = [new StringExtractor("A"), new NumberExtractor("X"), new NumberExtractor("C")] as const;

const output = mapExtractors(...input);
// output的类型会被正确推导为:
// [StringExtractorWithIndex, NumberExtractor, NumberExtractorWithIndex]

console.log(output[0].index); // 0(类型安全,不会报错)
console.log(output[1].index); // 运行时会抛出"No index",但类型上是NumberExtractor,符合预期
console.log(output[2].index); // 2(类型安全)

关键说明

  • 原函数编译失败的原因是:withIndex返回的是子类实例,但函数返回类型仍然是原类型[U, ...V],TypeScript无法识别这种类型替换。
  • 通过MappedExtractor条件类型,我们可以明确指定每个元素转换后的类型;MappedExtractorTuple则把这个映射应用到整个元组。
  • 函数内部的类型断言是必要的,因为TypeScript无法自动将first.withIndex(index)的返回类型与MappedExtractor<typeof first>关联起来,但我们通过类型定义已经保证了这个断言的安全性。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.16 09:54:56