Avatar ·

Как получить типы аргументов функции в TypeScript (Parameters, infer и условные типы)

Всем привет! Я пишу на TypeScript недавно и столкнулся с задачей, которая поставила меня в тупик. У меня есть несколько функций, которые принимают разные параметры. Например: ```typescript function fetchUser(id: number, name: string): void { // ... } function updateSettings(theme: 'light' | 'dark', fontSize: number): void { // ... } function logError(error: Error, source: string, timestamp: number): void { // ... } ``` Я хочу написать универсальную обёртку (wrapper), которая принимает любую функцию и её аргументы, делает какую-то предобработку (например, логирование), а затем вызывает оригинальную функцию. Проблема в том, что я не знаю, как динамически получить типы аргументов переданной функции. Вот что я пробовал: ```typescript function wrapFunction any>(fn: T, ...args: any[]) { console.log('Calling function with args:', args); return fn(...args); } ``` Это работает, но теряется типизация — `args` имеет тип `any[]`, и я не получаю подсказок от IDE. Я знаю, что в TypeScript есть какие-то условные типы и ключевое слово `infer`, но не могу разобраться, как их применить. Мне нужно, чтобы TypeScript автоматически выводил типы аргументов для каждой переданной функции. Чтобы при вызове `wrapFunction(fetchUser, 1, "Alice")` TypeScript проверял, что первый аргумент — `number`, а второй — `string`. И если я передам неправильные типы, компилятор выдавал ошибку. Подскажите, как это сделать? Буду благодарен за примеры кода и объяснение! <<>>

Использование встроенного типа 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 оправдано только когда нужно нетривиальное преобразование типов параметров.