Avatar ·

Ошибка TS2322: "could be instantiated with a different subtype of constraint 'object'" — как исправить?

Привет! Я новичок в TypeScript и столкнулся со странной ошибкой при использовании дженериков. У меня есть простая функция, которая должна принимать объект и возвращать его ключи. Я хочу, чтобы функция работала с любым объектом, но при этом сохраняла тип возвращаемого значения как массив ключей именно этого объекта. Вот мой код: ```typescript function getKeys(obj: T): (keyof T)[] { return Object.keys(obj) as (keyof T)[]; } function processObject(data: T) { // Тут я хочу передать data в другую функцию const handler = (input: U): void => { const keys = getKeys(input); console.log(keys); }; handler(data); // ОШИБКА здесь! } ``` TypeScript выдает ошибку: `Argument of type 'T' is not assignable to parameter of type 'U'. 'U' could be instantiated with a different subtype of constraint 'object'.` Я не понимаю, почему это происходит. Ведь и T, и U ограничены типом `object`. Почему TypeScript считает, что они могут быть разными? Как правильно переписать код, чтобы избежать этой ошибки? Заранее спасибо за помощь! <<>>

Понимание ошибки

Ошибка TS2322: "could be instantiated with a different subtype of constraint 'object'" возникает из-за того, что TypeScript рассматривает дженерики как независимые переменные типов. Даже если оба дженерика T и U ограничены extends object, они могут быть разными конкретными подтипами object.

В вашем примере: T — это тип, который будет определен при вызове processObject (например, {name: string; age: number}). U — это тип, который должен быть определен при вызове handler. TypeScript не может гарантировать, что T будет точно соответствовать U, поэтому он блокирует присваивание.

Решение 1: Убрать лишний дженерик

Самое простое решение — не создавать новый дженерик U, а использовать уже существующий T:

function processObject<T extends object>(data: T) {
  const handler = (input: T): void => {
    const keys = getKeys(input);
    console.log(keys);
  };

  handler(data); // ✅ Ошибки нет
}

Решение 2: Использовать infer или условные типы (для сложных случаев)

Если вам нужно сохранить обобщенность (например, если handler может вызываться с разными типами), используйте утилиту для вывода типа:

function processObject<T extends object>(data: T) {
  // Используем "infer" для вывода типа аргумента
  type HandlerInput = T;
  
  const handler = <U extends HandlerInput>(input: U): void => {
    const keys = getKeys(input);
    console.log(keys);
  };

  handler(data); // ✅ Работает
}

Решение 3: Снять ограничение и использовать any (не рекомендуется)

Хотя это решит ошибку, это снижает типобезопасность:

function processObject<T extends object>(data: T) {
  const handler = <U extends any>(input: U): void => {
    const keys = getKeys(input as object);
    console.log(keys);
  };

  handler(data); // ✅ Ошибки нет, но теряем типы
}

Почему это происходит?

  • TypeScript использует номинальную типизацию для дженериков — каждый дженерик считается уникальным типом, даже если они имеют одинаковые ограничения.
  • Ошибка защищает от ситуаций, когда T и U могут быть разными структурами данных, что привело бы к ошибкам во время выполнения.
  • Пример опасной ситуации: если бы T был {a: string}, а U — {b: number}, то внутри handler мы бы ожидали ключи от U, но получили бы ключи от T.

Лучшая практика

В большинстве случаев правильным решением будет избавиться от избыточного дженерика. Если вам нужно передать обобщенную функцию как колбэк, используйте сигнатуру с конкретным типом или переопределите дженерик через infer.

<<>>

Альтернативный подход: Использование перегрузки функций

Если вам нужно сохранить максимальную обобщенность, можно применить перегрузку:

function processObject<T extends object>(data: T): void;
function processObject(data: object): void {
  const handler = <U extends object>(input: U): void => {
    const keys = Object.keys(input) as (keyof U)[];
    console.log(keys);
  };
  handler(data);
}

Этот подход позволяет TypeScript "увидеть", что data имеет тип T, а не абстрактный object. Перегрузка сообщает компилятору: "первая сигнатура работает с любым T extends object, а реализация принимает просто object".

Дополнительное замечание

Обратите внимание, что Object.keys() возвращает string[], а не (keyof T)[]. Использование as (keyof T)[] — это приведение типа, которое может быть небезопасным. Рассмотрите использование Object.keys(obj) as Array<keyof T> для большей точности.