Learning Book

Как TypeScript выводит типы

Как TypeScript выводит типы

TypeScript спроектирован так, чтобы вы писали минимум аннотаций. Компилятор анализирует код и выводит типы самостоятельно. Этот процесс называется type inference – вывод типов.

Базовый вывод типов

При объявлении переменной с инициализатором TypeScript выводит тип из значения:

let x = 3;           // тип: number
let message = "hi";  // тип: string
let flag = true;     // тип: boolean

// const сужает тип до литерала
const y = 3;           // тип: 3 (литеральный тип)
const greeting = "hi"; // тип: "hi"

Вывод работает и для более сложных выражений:

// Тип выводится из структуры объекта
let user = {
  name: "Алиса",
  age: 30,
};
// тип: { name: string; age: number }

// Тип массива выводится из элементов
let numbers = [1, 2, 3]; // тип: number[]

Best Common Type

Когда TypeScript выводит тип из нескольких выражений (например, элементов массива), он ищет best common type – наилучший общий тип, совместимый со всеми кандидатами:

let zoo = [new Cat(), new Dog(), new Bird()];
// тип: (Cat | Dog | Bird)[]

TypeScript рассматривает каждый элемент как кандидата. Если ни один из кандидатов не является супертипом остальных, результат – union всех кандидатов.

// Если есть общий базовый тип, TypeScript его использует
class Animal { move() {} }
class Cat extends Animal { meow() {} }
class Dog extends Animal { bark() {} }

let pets = [new Cat(), new Dog()];
// тип: (Cat | Dog)[]
// НЕ Animal[], потому что Animal нет среди кандидатов

// Чтобы получить Animal[], укажите тип явно
let pets2: Animal[] = [new Cat(), new Dog()];
// тип: Animal[]

Важно: best common type выбирается из числа кандидатов. Если общий базовый тип не представлен среди элементов, TypeScript не «угадывает» его – он объединяет кандидатов в union.

Contextual Typing (Контекстный вывод)

Контекстный вывод типов работает в обратном направлении – TypeScript определяет тип выражения из позиции, в которой оно используется:

// TypeScript знает, что параметр e -- это MouseEvent,
// потому что addEventListener ожидает обработчик для "mousedown"
window.addEventListener("mousedown", function (e) {
  console.log(e.button); // OK -- e: MouseEvent
});

Контекстный вывод применяется в множестве ситуаций:

// Аргументы callback-функций
const names = ["Алиса", "Борис", "Вера"];
names.forEach(function (name) {
  console.log(name.toUpperCase()); // name: string
});

// Стрелочные функции
names.map((name) => name.length); // name: string, результат: number[]

// Правая часть присваивания с аннотированным типом
const handler: (event: KeyboardEvent) => void = (e) => {
  console.log(e.key); // e: KeyboardEvent
};

Когда контекстный вывод не работает

Если функция находится в позиции без контекстного типа, параметры получают тип any (или ошибку при noImplicitAny):

// Нет контекста -- TypeScript не знает тип x
// Ошибка: Parameter 'x' implicitly has an 'any' type
function double(x) {
  return x * 2;
}

// Исправление: добавьте аннотацию
function double(x: number): number {
  return x * 2;
}

Вывод типов в дженериках

При вызове generic-функции TypeScript выводит параметры типов из переданных аргументов:

function identity<T>(value: T): T {
  return value;
}

// T выводится как string
const result = identity("hello"); // тип: string

// T выводится как number
const num = identity(42); // тип: number

Вывод из нескольких аргументов

Когда один параметр типа используется в нескольких позициях, TypeScript объединяет информацию:

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

// T = string
merge("a", "b"); // OK: string[]

// T = string | number (TypeScript объединяет кандидатов)
merge("a", 1); // OK: (string | number)[]

Вывод с ограничениями

Ограничения (extends) участвуют в процессе вывода:

function longest<T extends { length: number }>(a: T, b: T): T {
  return a.length >= b.length ? a : b;
}

// T выводится как string
longest("abc", "de"); // OK: string

// T выводится как number[]
longest([1, 2, 3], [4, 5]); // OK: number[]

// Ошибка: number не имеет свойства length
// longest(10, 20);

Вывод с несколькими параметрами типов

function map<Input, Output>(
  arr: Input[],
  fn: (item: Input) => Output,
): Output[] {
  return arr.map(fn);
}

// Input = string, Output = number
const lengths = map(["hello", "world"], (s) => s.length);
// тип: number[]

Здесь TypeScript выводит Input из первого аргумента (массив строк), а Output – из возвращаемого типа callback-функции.

Вывод типа возвращаемого значения

TypeScript выводит тип возвращаемого значения функции из её тела:

// Возвращаемый тип: string
function greet(name: string) {
  return `Привет, ${name}!`;
}

// Возвращаемый тип: number | string
function format(value: number | string) {
  if (typeof value === "number") {
    return value.toFixed(2); // string
  }
  return value; // string
}

Практика: для публичных API-функций рекомендуется явно указывать возвращаемый тип. Это защищает от случайного изменения контракта и улучшает сообщения об ошибках.

Типичные ловушки вывода типов

Расширение литеральных типов (Widening)

const x = "hello";  // тип: "hello" (литерал)
let y = "hello";    // тип: string (расширен)

// Объект: свойства расширяются
const config = {
  mode: "development", // тип: string, а не "development"
  port: 3000,          // тип: number, а не 3000
};

// Решение: as const
const config2 = {
  mode: "development",
  port: 3000,
} as const;
// тип: { readonly mode: "development"; readonly port: 3000 }

Вывод any из пустых коллекций

// Пустой массив без аннотации -- тип any[]
const items = []; // тип: any[] (при noImplicitAny -- ошибка)

// TypeScript "расширяет" тип по мере добавления элементов
items.push(1);      // тип: number[]
items.push("hello"); // тип: (number | string)[]

// Лучше: задайте тип явно
const items2: number[] = [];

Итоги

МеханизмНаправлениеПример
Базовый выводЗначение -> типlet x = 3 -> number
Best common typeНесколько значений -> union[1, "a"] -> (number | string)[]
Contextual typingКонтекст -> параметрыarr.map(x => ...)
Вывод в дженерикахАргументы -> параметры типовidentity("hi") -> T = string
Вывод return typeТело функции -> возвращаемый типreturn x + 1 -> number

Алгоритм вывода типов внутри компилятора

Когда TypeScript выводит параметры типа generic-функции, он использует алгоритм унификации:

  1. Сбор кандидатов – для каждого параметра типа T компилятор собирает все типы, которые T мог бы принять, на основе позиций использования.

  2. Ковариантные позиции (возвращаемые типы, свойства) дают нижнюю границу – T должен быть как минимум этим типом.

  3. Контравариантные позиции (параметры функций) дают верхнюю границу – T должен быть не более общим, чем этот тип.

  4. Выбор результата – если все кандидаты совместимы, TypeScript выбирает наиболее специфичный. Если нет – объединяет в union.

Этот процесс происходит рекурсивно для вложенных типов. Когда параметр типа появляется в нескольких позициях с противоречивыми требованиями, TypeScript может выдать ошибку совместимости.