如何用可变元组类型递归转换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
相关产品推荐
相关产品推荐

