Перейти к содержимому
Теория · Дженерики · урок 3 из 10

Как выводится T

В прошлом уроке TypeScript сам понял, что first([100, 250]) возвращает number | undefined, хотя никто не писал first<number>. Откуда он берёт тип для T?

Вернёмся в порт. Этикетку в рамке на двери контейнера пишет грузчик: он видит, что кладут внутрь, и заполняет рамку. При вызове обобщённой функции грузчиком работает TypeScript. Он смотрит на аргументы и сам решает, какой тип подставить вместо T. Каждый вызов — новая погрузка с новой этикеткой.

Тип из аргументов

Когда вы вызываете first([100, 250]), TypeScript сопоставляет аргумент с параметром:

  1. Параметр объявлен как xs: T[] — массив элементов типа T.
  2. Аргумент [100, 250] — массив чисел, number[].
  3. Чтобы number[] совпал с T[], на месте T должен стоять number. Это и есть вывод аргумента типа: TypeScript находит T по типам аргументов.

Вывод идёт заново при каждом вызове. Прошлый вызов не влияет на следующий: если затем вызвать first(["Paris", "Rome"]), T станет string, а у первого вызова останется number. Поэтому одна функция возвращает точный тип для разных входов.

Если в массиве значения разных типов, T получает их объединение. У литерала [1, "a"] тип (string | number)[], значит T — это string | number.

Пройдите программу по шагам. Следите за подсказкой у переменных с результатом: она показывает, каким стал T в этом вызове.

ВживуюЧто TypeScript подставил вместо Tшаг 1 из 7

Внутри объявления T — пустая рамка: какой там будет тип, решит вызов.

Нажимайте «Следующий шаг» и следите, каким стал T в каждом вызове

Два параметра типа

Параметров типа может быть несколько. Каждый из них TypeScript выводит из своего аргумента, независимо от остальных:

TypeScript
function pair<A, B>(a: A, b: B): [A, B] {
    return [a, b];
}

const entry = pair("Ann", 30);
console.log(entry[0], entry[1]); // Ann 30

Разберём, что здесь происходит:

  1. <A, B> объявляет два параметра типа через запятую.
  2. Из "Ann" TypeScript выводит A — string, из 30 выводит B — number.
  3. Результат объявлен как кортеж [A, B], поэтому у entry тип [string, number].
  4. console.log печатает Ann 30.

Когда выводить не из чего

У функции createList из примера выше нет параметров, и T нигде среди аргументов не встречается. Если результат кладут в переменную без аннотации, рамку заполнить нечем, и TypeScript ставит в неё unknown:

TypeScript
function createList<T>(): T[] {
    return [];
}

const items = createList();
const nums = createList<number>();
nums.push(5);
console.log(items.length, nums.length); // 0 1

У items тип unknown[]. В такой массив можно положить что угодно, а вот воспользоваться элементом без проверки нельзя. Для nums аргумент типа написан явно, и это обычный number[]: в него кладут только числа. Тот же тип даст и аннотация переменной: в const ids: number[] = createList() TypeScript берёт T из того, куда идёт результат.

Здесь аналогия с грузчиком заканчивается. Настоящий грузчик заглянет в пустой контейнер и спросит, что в него положат потом. TypeScript решает всё в момент вызова: строки после него на T не влияют. Если ниже написать items.push(5), у items всё равно останется тип unknown[].

На собеседовании часто спрашивают, как TypeScript выбирает T, если вызвать обобщённую функцию без угловых скобок. Ответ: выводит его из аргументов этого вызова, а если в них T нет — из аннотации переменной, куда идёт результат; без неё T становится unknown.

Частая ошибка

В функцию с одним T на два параметра передали значения разных типов:

TypeScript
function both<T>(a: T, b: T): T[] {
    return [a, b];
}

console.log(both(1, "a")); // ✗
TEXT
error TS2345: Argument of type 'string' is not assignable to parameter of type 'number'.

Кажется, что T станет string | number, как у массива [1, "a"]. Но T — один тип на весь вызов: TypeScript взял number из первого аргумента, и строка под него не подошла. Внутри одного литерала массива TypeScript объединяет типы элементов, а для двух отдельных аргументов одного T — нет. Если функция должна принять оба типа, скажите это явно: both<string | number>(1, "a").

Главное

  • TypeScript выводит T из аргументов вызова: first([100, 250]) даёт number | undefined, а first(["Paris"]) — string | undefined.
  • Вывод идёт заново при каждом вызове, поэтому одна функция возвращает точный тип для разных входов.
  • Несколько параметров типа выводятся независимо: pair("Ann", 30) даёт [string, number].
  • Если T выводить не из чего — ни из аргументов, ни из аннотации переменной, — он становится unknown; тогда аргумент типа пишут явно: createList<number>().