typescript 是否可以让rest参数具有重复类型?

xtfmy6hx  于 2023-05-08  发布在  TypeScript
关注(0)|答案(1)|浏览(88)

我正在尝试弄清楚我是否可以编写一个类型安全的函数,它接受重复的varargs参数。例如,它接受1+对stringnumber。你可以这样称呼它:

declare function repeatingRestParamsPairs(...args: unknown[] /* What type should I use here? */): void;

repeatingRestParamsPairs(); // error
repeatingRestParamsPairs("a"); // error
repeatingRestParamsPairs("a", 1); // compiles
repeatingRestParamsPairs("a", 1, "b"); // error
repeatingRestParamsPairs("a", 1, "b", 2); // compiles
repeatingRestParamsPairs("a", 1, "b", 2, "c"); // error
repeatingRestParamsPairs("a", 1, "b", 2, "c", 3); // compiles

repeatingRestParams的实现中,args[oddNumber]将是stringargs[evenNumber]number
请注意,我使用的stringnumber对仅用于说明目的。理想情况下,该解决方案将允许三重和更长的重复模式。它还允许接口、联合类型和交集类型用作重复模式中的类型,例如:

type FirstInPattern = "a" | "b" | "c" // the first in the pattern should be one of these

declare function repeatingRestParamsTriple(...args: unknown[] /* What type should I use here? */): void;

repeatingRestParamsTriple(); // error
repeatingRestParamsTriple("a"); // error
repeatingRestParamsTriple("a", 1); // error
repeatingRestParamsTriple("a", 1, 2); // compiles
repeatingRestParamsTriple("foo", 1, 2); // error
repeatingRestParamsTriple("a", 1, 2, "b"); // error
repeatingRestParamsTriple("a", 1, 2, "b", 3); // error
repeatingRestParamsTriple("a", 1, 2, "b", 3, 4); // compiles
repeatingRestParamsTriple("a", 1, 2, "b", 3, 4, "c"); // error
repeatingRestParamsTriple("a", 1, 2, "b", 3, 4, "c", 5); // error
repeatingRestParamsTriple("a", 1, 2, "b", 3, 4, "c", 5, 6); // compiles

我对此做了很多研究,但我不知道这是否可行;这就像试图证明一个否定。我发现的最接近的是variadic tuple types,但我没有看到任何重复模式的例子。
我也试过将...args: unknown[]更改为...args: [argType1, argType2]。它扩展为只接受两个参数的函数。并且...args: [argType1, argType2][]展开为以[argType1, argType2]的变进数为参数的函数;我想要argType1, argType2, argType1?, argType2?, ...作为参数。这可能吗?

xyhw6mcr

xyhw6mcr1#

TypeScript中没有 specific 类型对应于通过将特定元组连接在一起任意正数次而形成的所有元组。如果你想要支持的最大重复次数比较小,你可以编写一个实用程序类型来组装一个合适的联合:

type RepeatUpTo<T extends any[], N extends number, A extends any[][] = [T]> =
  A['length'] extends N ? A[number] : RepeatUpTo<T, N, [[...A[0], ...T], ...A]>

type Demo = RepeatUpTo<[0, 1, 2], 4>
// type Demo = [0, 1, 2] | [0, 1, 2, 0, 1, 2] | [0, 1, 2, 0, 1, 2, 0, 1, 2] | 
//  [0, 1, 2, 0, 1, 2, 0, 1, 2, 0, 1, 2];

对于[string, number],你可以这样写repeatingChunk()

type Chunk = [string, number];
declare function repeatingChunk(...args: RepeatUpTo<Chunk, 10>): void;

这对你所有的例子都有效

repeatingChunk(); // error
repeatingChunk("a"); // error
repeatingChunk("a", 1); // compiles
repeatingChunk("a", 1, "b"); // error
repeatingChunk("a", 1, "b", 2); // compiles
repeatingChunk("a", 1, "b", 2, "c"); // error
repeatingChunk("a", 1, "b", 2, "c", 3); // compiles

但如果超过上述任意选择的最大重复次数,则失败:

repeatingChunk("", 0, "", 0, "", 0, "", 0, "", 0, "", 0, "", 
  0, "", 0, "", 0, "", 0, "", 0); // error! 😢
// Source has 22 element(s) but target allows only 20.

所以这取决于你的用例;你真的需要支持 * 任意多 * 的重复吗?哦,至于实现,有一些好消息和一些坏消息:

function repeatingChunk(...args: RepeatUpTo<Chunk, 10>) { {
  args[0].toUpperCase(); // 👍
  args[1].toFixed(); // 👍
  args[2]?.toUpperCase(); // 👍
  args[3]?.toFixed(); // 👍
  args[19]?.toFixed(); // 👍
  args[20]?.toUpperCase(); // 👎
  for (let i = 0; i < args.length / 2; i++) {
    args[2 * i].toUpperCase(); // 👎
    args[2 * i + 1].toFixed(); // 👎
  }
}

虽然编译器知道 specific 偶数和奇数索引具有stringnumber(或undefined)值,但它不知道如何将其推广到任何类似循环的代码。类型系统没有执行高阶数学分析来知道2 * i将是偶数,并且小于长度的偶数索引具有string值。因此,即使在编译器知道很多关于输入的信息的最佳情况下,您仍然需要类型Assert来继续。

for (let i = 0; i < args.length / 2; i++) {
  (args[2 * i] as string).toUpperCase();
  (args[2 * i + 1] as number).toFixed();
}

无论如何,如果你真的需要支持 * 任意多次 * 重复,你就必须给予特定的类型,转而编写一个generic类型,并使repeatingChunk成为一个泛型函数。这里有一个方法:

type Chunk = [string, number];
type _Repeating<T> = T extends [...Chunk, ...infer R] ?
  [string, number, ..._Repeating<R>] : [];
type Repeating<T> = [...Chunk, ...any[]] & _Repeating<T>

declare function repeatingChunk<T extends any[]>(
  ...args: T extends Repeating<T> ? T : Repeating<T>): void;

这里的Repeating<T>是一个conditional type,它验证TChunk副本的有效级联。
args的类型是T extends Repeating<T> ? T : Repeating<T>,它看起来很奇怪,但正被用来指导推理。理想情况下,我们希望约束T extends Repeating<T>,但这不起作用,因为它是一个循环约束。然后,您可能希望将args的类型设置为Repeating<T>,但这也不起作用(尝试一下并查看)。我使用的一种技术是从T extends F<T> ? T : F<T>形式的内联条件类型进行推断。编译器查看T extends ⋯并从函数参数的类型推断T(在本例中是args rest参数)。然后,它将其与T extends F<T> ? T : F<T>进行 * 检查 *。如果它成功了,那么这就相当于T,并且它会继续成功。如果它失败了,那么这相当于F<T>,并且它继续失败。我不知道这种技术在任何地方都有正式记录;这只是从这些复杂的条件类型推断泛型时可能发生的另一件烦人的事情。
让我们看看它是否有效:

repeatingChunk(); // error
repeatingChunk("a"); // error
repeatingChunk("a", 1); // compiles
repeatingChunk("a", 1, "b"); // error
repeatingChunk("a", 1, "b", 2); // compiles
repeatingChunk("a", 1, "b", 2, "c"); // error
repeatingChunk("a", 1, "b", 2, "c", 3); // compiles

很好...它仍然适用于更长的参数列表:

repeatingChunk("", 0, "", 0, "", 0, "", 0, "", 0, "", 0, "", 
  0, "", 0, "", 0, "", 0, "", 0); // compiles

所以这适用于任意长的输入列表…打电话的人
执行方面仍然存在问题,而且这些问题更加明显。现在只有01索引是强类型的。其余的被推断为never类型,因为编译器在分析泛型条件类型方面非常糟糕:

function repeatingChunk<T extends any[]>(
  ...args: T extends Repeating<T> ? T : Repeating<T>) {
  args[0].toUpperCase(); // 👍
  args[1].toFixed(); // 👍
  args[2]?.toUpperCase(); // 👎
  args[3]?.toFixed(); // 👎
  args[19]?.toFixed(); // 👎
  args[20]?.toUpperCase(); // 👎
  for (let i = 0; i < args.length / 2; i++) {
    args[2 * i].toUpperCase(); // 👎
    args[2 * i + 1].toFixed(); // 👎
  }
}

所以这里也需要使用类型Assert来在函数内部取得进展:

for (let i = 0; i < args.length / 2; i++) {
  (args[2 * i] as string).toUpperCase();
  (args[2 * i + 1] as number).toFixed();
}

可能有一些方法可以使实现不那么痛苦,但它们都将涉及到一些类型安全性的损失。我能想象的唯一改进方法是TypeScript是否支持“重复元组”类型,这是不太可能的。* 以及 * 一种将它们分布到强类型块中的方法(或者理解2*i+1的数学),这更不可能。
Playground链接到代码

相关问题