关于 variadic-tuple-types
面向 TypeScript 开发者的技能,讲解如何为可变参数函数添加精确类型,并用剩余参数、元组和泛型保留参数类型,涵盖组合与柯里化模式。
主要功能
- 01精确标注可变参数函数
- 02用元组保留参数类型
- 03用泛型处理类型转换
- 04将元组用于组合与柯里化
适用场景
- 01为可变参数函数添加类型
- 02构建类型安全的函数组合工具
- 03实现类型安全的柯里化
- 04用元组建模事件参数
安装
复制下面的命令,在终端中运行。
$
npx skills add https:/ / github.com/ marius-townhouse/ effective-typescript-skills --skill variadic-tuple-types安装后在 Claude Code 中输入/variadic-tuple-types确认已加载。
详细介绍
使用剩余参数和元组类型为可变参数函数建模
概述
可变参数函数是指接受数量不定的参数的函数,在 JavaScript 中很常见。TypeScript 的元组类型和剩余参数可以让你精确地为这类函数添加类型,同时保留参数的数量和类型。这使得类型化函数组合和泛型管道等强大模式成为可能。
了解如何结合使用剩余参数和元组类型,可以为灵活的 API 实现精确的类型定义。
何时使用此技能
- 函数接受数量不定且带有类型的参数
- 需要在转换过程中保留元组长度
- 构建泛型函数组合工具
- 为带有特定约束的剩余参数添加类型
- 创建类型化函数管道
铁律
使用元组类型为剩余参数添加类型,从而精确描述可变参数函数。结合泛型,在转换过程中保留参数类型。
检测
留意以下未添加类型的模式:
// RED FLAGS - Untyped variadic functions
function logAll(...args: any[]): void; // Lost type information
function compose(...fns: Function[]): Function; // No type safety
function curry(fn: Function): Function; // Arguments not tracked
使用元组的基本剩余参数
// Preserve exact argument types
function logAll<T extends any[]>(...args: T): void {
console.log(args);
}
logAll(1, 'hello', true);
// T is inferred as [number, string, boolean]
// Constrain the tuple
type StringNumberPair = [string, number];
function formatPair(...pair: StringNumberPair): string {
return `${pair[0]}: ${pair[1]}`;
}
formatPair('score', 100); // OK
formatPair('score'); // Error: missing number
formatPair('score', 100, 'extra'); // Error: too many arguments
泛型可变参数函数
// Preserve types through transformation
function tail<T extends any[]>(
head: T[0],
...rest: T extends [any, ...infer R] ? R : never
): T {
return [head, ...rest] as T;
}
const result = tail(1, 'a', true);
// result: [number, string, boolean]
函数组合
// Compose functions with preserved types
type Fn = (...args: any[]) => any;
function compose<T extends Fn[]>(
...fns: T
): (...args: Parameters<T[0]>) => ReturnType<T[number]> {
return (arg) => fns.reduceRight((acc, fn) => fn(acc), arg);
}
const add1 = (x: number) => x + 1;
const double = (x: number) => x * 2;
const toString = (x: number) => String(x);
const composed = compose(toString, double, add1);
// (x: number) => string
const result = composed(5); // "12"
使用元组类型实现柯里化
// Type-safe currying
type Curry<T extends any[], R> = T extends [infer First, ...infer Rest]
? (arg: First) => Rest extends [] ? R : Curry<Rest, R>
: () => R;
function curry<T extends any[], R>(
fn: (...args: T) => R
): Curry<T, R> {
return ((arg: any) => {
if (arguments.length >= fn.length) {
return fn(...arguments);
}
return curry(fn.bind(null, arg));
}) as Curry<T, R>;
}
const sum3 = (a: number, b: number, c: number) => a + b + c;
const curried = curry(sum3);
const result = curried(1)(2)(3); // 6
// Each step is correctly typed!
实际示例:事件发射器
type EventMap = {
click: [x: number, y: number];
keypress: [key: string];
load: [];
};
class TypedEmitter<Events extends Record<string, any[]>> {
emit<K extends keyof Events>(
event: K,
...args: Events[K]
): void {
// Implementation
}
on<K extends keyof Events>(
event: K,
handler: (...args: Events[K]) => void
): void {
// Implementation
}
}
const emitter = new TypedEmitter<EventMap>();
emitter.emit('click', 100, 200); // OK
emitter.emit('click', 100); // Error: missing y
emitter.on('keypress', (key) => {
console.log(key); // key is string
});
抗压协议
为可变参数函数添加类型时:
- 使用元组约束:使用
[string, number],而不是string | number - 通过泛型保留类型:
T extends any[]可以捕获确切类型 - 考虑长度:使用元组长度定义条件类型
- 测试边界情况:空元组、单个元素、多个元素
危险信号
| 反模式 | 问题 | 解决方案 |
|---|---|---|
...args: any[] | 丢失类型信息 | T extends any[] |
Function 类型 | 没有参数和返回值类型 | 泛型函数类型 |
| 使用数组而不是元组 | 丢失长度信息 | 元组类型 |
常见借口
“这对我的使用场景来说太复杂了”
实际情况:从 T extends any[] 开始,仅在需要时增加复杂度。
“用户可以把参数作为数组传递”
实际情况:剩余参数使用起来更顺手。正确地为其添加类型,才能获得最佳开发体验。
“我直接使用重载就行了”
实际情况:元组可以处理通用情况。重载则需要手动枚举。
快速参考
| 模式 | 语法 | 使用场景 |
|---|---|---|
| 捕获参数 | T extends any[] | 保留参数类型 |
| 限制长度 | [A, B, C] | 固定数量的参数 |
| 最小长度 | [A, B, ...C[]] | 至少 2 个参数 |
| 转换 | Parameters<T[0]> | 提取参数类型 |
总结
使用元组类型的剩余参数可以精确地为可变参数函数添加类型。使用泛型在转换过程中保留参数类型,从而实现类型化组合等强大模式。
参考资料
- Dan Vanderkam 著《Effective TypeScript》第 2 版
- 条目 62:使用剩余参数和元组类型为可变参数函数建模
评论
支持 Markdown