Как получить типы аргументов функции в TypeScript (Parameters, infer и условные типы)
Использование встроенного типа Parameters
В TypeScript есть встроенный утилитарный тип Parameters<T>, который извлекает типы параметров функции в виде кортежа. Это самое простое и элегантное решение вашей задачи.
type FetchUserParams = Parameters<typeof fetchUser>;
// type FetchUserParams = [id: number, name: string]
type UpdateSettingsParams = Parameters<typeof updateSettings>;
// type UpdateSettingsParams = [theme: 'light' | 'dark', fontSize: number]
type LogErrorParams = Parameters<typeof logError>;
// type LogErrorParams = [error: Error, source: string, timestamp: number]
Теперь применим это к вашей функции-обёртке:
function wrapFunction<T extends (...args: any[]) => any>(
fn: T,
...args: Parameters<T>
): ReturnType<T> {
console.log('Calling function with args:', args);
return fn(...args);
}
// Примеры использования с полной типовой безопасностью:
wrapFunction(fetchUser, 1, "Alice"); // ✅ OK
wrapFunction(fetchUser, "1", "Alice"); // ❌ Ошибка: 'string' не присваивается 'number'
wrapFunction(fetchUser, 1); // ❌ Ошибка: ожидается 2 аргумента, получен 1
wrapFunction(updateSettings, 'dark', 14); // ✅ OK
wrapFunction(updateSettings, 'blue', 14); // ❌ Ошибка: 'blue' не входит в union тип
wrapFunction(logError, new Error('fail'), 'module.ts', Date.now()); // ✅ OK
Как это работает под капотом?
Тип Parameters<T> внутри использует условный тип с infer:
type Parameters<T extends (...args: any) => any> =
T extends (...args: infer P) => any ? P : never;
Разберём по шагам:
- Условный тип:
T extends (...args: infer P) => any ? P : never— проверяет, является ли T функцией. - Ключевое слово
infer: создаёт новую переменную типаP, которая будет равна кортежу параметров функции. - Ветвление: если T — функция, возвращаем
P(типы параметров), иначе —never.
Расширенный пример: обработка аргументов перед вызовом
Вы можете не только логировать, но и модифицировать аргументы:
function withLogging<T extends (...args: any[]) => any>(
fn: T
): (...args: Parameters<T>) => ReturnType<T> {
return (...args: Parameters<T>) => {
console.log(`[${new Date().toISOString()}] Calling ${fn.name}`);
const result = fn(...args);
console.log(`[${new Date().toISOString()}] Result:`, result);
return result;
};
}
const loggedFetch = withLogging(fetchUser);
loggedFetch(1, "Alice"); // ✅ Строгая типизация + логирование
Работа с методами классов и контекстом
Если нужно сохранить контекст this:
function bindWithTypes<T extends (this: any, ...args: any[]) => any>(
fn: T,
context: ThisParameterType<T>
): (...args: Parameters<T>) => ReturnType<T> {
return fn.bind(context);
}
class UserService {
constructor(private users: Map<number, string>) {}
getUser(id: number): string | undefined {
return this.users.get(id);
}
}
const service = new UserService(new Map([[1, 'Alice']]));
const boundGetUser = bindWithTypes(service.getUser, service);
boundGetUser(1); // ✅ Тип: (id: number) => string | undefined
Используйте Parameters<T> везде, где нужно получить типы аргументов функции. Это стандартный, хорошо документированный и производительный подход.
Альтернативный подход: ручное использование infer в условных типах
Если по какой-то причине вы не хотите использовать встроенный Parameters (например, для образовательных целей или для кастомной обработки), можно реализовать его самостоятельно:
type MyParameters<T> =
T extends (...args: infer P) => any ? P : never;
// То же самое, что и Parameters<T>
type FetchParams = MyParameters<typeof fetchUser>;
// [id: number, name: string]
Когда это может быть полезно?
- Фильтрация параметров: можно извлечь только часть параметров:
type FirstParam<T> =
T extends (first: infer F, ...rest: any[]) => any ? F : never;
type SecondParam<T> =
T extends (first: any, second: infer S, ...rest: any[]) => any ? S : never;
type First = FirstParam<typeof fetchUser>; // number
type Second = SecondParam<typeof fetchUser>; // string
- Трансформация параметров: например, сделать все параметры опциональными:
type OptionalParams<T> =
T extends (...args: infer P) => any
? Partial<P>
: never;
type OptionalFetchParams = OptionalParams<typeof fetchUser>;
// Partial<[id: number, name: string]> = [id?: number, name?: string]
Ограничения ручного подхода
Встроенный Parameters оптимизирован и учитывает все краевые случаи (например, перегруженные функции). Для повседневного использования рекомендую именно его. Ручное использование infer оправдано только когда нужно нетривиальное преобразование типов параметров.