open atlas
↑ К треку
Система типов TypeScript вглубь TS · 01 · 02

Основы вывода: как проверщик заполняет типы, которые вы не написали

TypeScript выводит большинство типов, чтобы их не приходилось писать. Правила — типизация по инициализатору, расширение при const против let, контекстная типизация колбэков, наилучший общий тип массивов — предсказуемы, стоит их увидеть. Аннотируйте границы, выводите локальные.

TS Junior ◷ 13 min
Уровень
ОсновыJuniorMiddleSenior

Вы добавляете : string к локальной переменной, и ревьюер пишет комментарий: «избыточно — TS уже знает». А на следующей строке вы пишете const handler = items.map(x => x.toUpperCase()), и параметр колбэка x типизируется правильно вообще без аннотации. TypeScript не угадывает; он гоняет небольшой детерминированный алгоритм вывода. Изучите его правила — и будете аннотировать ровно там, где это окупается, и нигде больше.

Вывод из инициализатора

Когда вы объявляете переменную со значением, TypeScript выводит её тип из правой части. Но точный тип зависит от ключевого слова связывания из-за расширения (widening):

let a = "hello";
//  ^? let a: string  — `let` изменяем, поэтому литерал расширяется до базового типа

const b = "hello";
//    ^? const b: "hello"  — `const` не может измениться, литеральный тип сохраняется

Это самое важное правило вывода, которое стоит впитать. let можно переприсвоить, поэтому привязка его к литералу "hello" была бы бесполезна — TS расширяет до string. const неизменяем, поэтому точный литеральный тип "hello" и безопасен, и информативнее. То же с числами и булевыми: let n = 3 это number, const n = 3 это 3.

Свойства объекта расширяются — даже под const

Вот сюрприз, на котором спотыкаются все. const на связывании не делает содержимое неизменяемым, потому что свойства объекта изменяемы:

const config = { mode: "dark", retries: 3 };
//    ^? const config: { mode: string; retries: number }
//       НЕ { mode: "dark"; retries: 3 } — свойства расширяются
config.mode = "light"; // законно — свойство изменяемо, поэтому его тип расширен до `string`

const замораживает лишь ссылку config, а не config.mode. Поскольку свойство можно переприсвоить, TS расширяет его до string. Когда литеральные типы действительно нужно сохранить — скажем, чтобы передать в функцию, ожидающую объединение, — используйте утверждение const:

const config = { mode: "dark", retries: 3 } as const;
//    ^? const config: { readonly mode: "dark"; readonly retries: 3 }
config.mode = "light";
// Error: Cannot assign to 'mode' because it is a read-only property.

as const делает каждое свойство readonly и сохраняет его литеральный тип. Глубоко это разберём в уроке о литеральных типах; пока знайте — это лекарство, когда расширение выбрасывает информацию, которая вам была нужна.

Контекстная типизация: колбэки получают типы бесплатно

Вывод также течёт внутрь от ожидаемого типа. Когда функция ждёт колбэк, параметры колбэка типизируются из этого ожидания — вы их не аннотируете:

const names = ["ada", "alan", "grace"];
const upper = names.map((n) => n.toUpperCase());
//                       ^? (parameter) n: string
//    ^? const upper: string[]

n это string, потому что Array<string>.map объявляет свой колбэк как (value: string, ...) => U. Это контекстная типизация: позиция, в которой находится функциональный литерал, поставляет типы параметров. Поэтому DOM-обработчики тоже работают без аннотаций:

button.addEventListener("click", (e) => {
  //                                ^? (parameter) e: MouseEvent
  console.log(e.clientX);
});

Строка "click" выбирает перегрузку, чей обработчик принимает MouseEvent, и это течёт в e. Аннотация e: MouseEvent здесь — избыточный шум.

Почему это работает

Контекстная типизация работает, только когда ожидаемый тип виден в точке написания функционального литерала. Вынесите колбэк в отдельный const fn = (e) => ... — и контекста больше нет: e падает в неявный any (ошибка при noImplicitAny). Такова сделка: встроенные колбэки получают типизацию бесплатно; вынесенным нужна явная аннотация параметра или типизированный псевдоним.

Наилучший общий тип для литералов массива

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

const xs = [1, 2, 3];
//    ^? const xs: number[]

const mixed = [1, "two", 3];
//    ^? const mixed: (string | number)[]  — объединение типов элементов

Если у элементов нет общего супертипа в области видимости, TS образует их объединение. Заметьте: const на связывании не сохраняет [1,2,3] как кортеж [number, number, number] — это по-прежнему number[]; только as const даёт кортеж readonly [1, 2, 3] (разбираем в уроке о литеральных типах).

Типы возврата тоже выводятся

Вы почти никогда не аннотируете тип возврата для внутренних хелперов — TS выводит его из операторов return:

function parsePort(raw: string) {
  const n = Number(raw);
  return Number.isInteger(n) ? n : null;
}
//       ^? function parsePort(raw: string): number | null

Выведенный возврат — number | null из двух ветвей. Это отлично для локального кода — но острый инструмент на границах API.

Когда аннотировать, а когда выводить

Это вопрос, который рано или поздно ставит каждая TypeScript-кодовая база: аннотируешь лишнее — борешься с проверщиком; аннотируешь мало — теряешь контракт. Вот правило, к которому сходятся профессиональные кодовые базы:

СитуацияАннотировать?Почему
Локальная переменная с инициализаторомНетВывод точен и остаётся синхронным со значением
Параметр встроенного колбэкаНетЕго поставляет контекстная типизация
Тип возврата экспортируемой функцииДаФиксирует публичный контракт; не даёт внутреннему изменению тихо расширить API
Параметры функцииДаПараметры — входная граница; вывод не видит вызывающих
Пустой let x; с присваиванием позжеДаГолый let выводит any; аннотация сохраняет проверку

Принцип: аннотируйте границы, выводите внутренности. Явные типы возврата экспортируемых функций — аннотации наивысшей ценности: они делают контракт намеренным и ловят случайное расширение при рефакторинге тела.

Почему это работает

Где вывод вредит, с числами. Вывод бесплатен, пока он локален, но на границах модулей у него реальная, измеримая цена. Когда у экспортируемой функции нет аннотации возврата, tsc вынужден заново выводить этот тип возврата для каждого файла, который её импортирует, и сложные выведенные возвраты (глубокие дженерики, условные типы, длинные объединения) — классическая причина медленных сборок. На репозитории ~900 файлов React + tRPC (TS 5.5) один слишком умный хелпер, чей тип возврата вывелся в условно-отображённый тип из 30 членов, поднял tsc --noEmit с 9.4с до 16.1с; --extendedDiagnostics показал, что «Check time» почти удвоилось, а число инстанцирований подскочило до ~3.4M против потолков 100k на проверку / 5M отношений, при этом подсказка IntelliSense на местах вызова отставала на ~600мс+, пока редактор пересчитывал тип. Закрепление написанного руками типа возврата вернуло его к ~9.6с. Компромисс сеньора: на внутренних местах вызова доверяйте выводу — аннотации там шум, который рассинхронизируется; на экспортируемых границах заплатите одной аннотацией — три нажатия в обмен на стабильный публичный контракт, более быстрые инкрементальные проверки и более отзывчивый редактор у каждого потребителя.

Викторина

Каков выведенный тип `mode` в `const config = { mode: 'dark' }` (без `as const`)?

Закончи аналогию

Параметр колбэка, получающий тип от функции, в которую он передан — без какой-либо аннотации — типизируется ____ типизацией.

Вспомните перед уходом
  1. 01
    Объясните расширение при const против let и почему свойства объекта расширяются, даже когда связывание — const.
  2. 02
    Что такое контекстная типизация, где она применяется и когда перестаёт работать?
  3. 03
    Какова практическая политика аннотаций — что аннотировать, а что доверить выводу и почему?
Итог

Вывод — причина, почему TypeScript не мешает вам: формы, которые вы научились разбирать структурно в прошлом уроке, по большей части заполняются за вас, а тяжёлую работу делают расширение и контекстная типизация. Правила расширения, которые вы только что увидели — const, хранящий "a", let, расширяющий до string, расширение свойств, as const как лазейка — это ровно та механика, на которой строится следующий урок о литеральных типах. Но сперва заглянем в три особых типа — any, unknown и never — потому что понимание того, как вывод и совместимость ведут себя на вершине и дне решётки типов, и удерживает any от тихого отравления всего, что вывод вам только что дал. Теперь, когда видите комментарий ревьюера «избыточная аннотация», проверьте, не покрывает ли вывод этот слот уже — а когда tsc тормозит, посмотрите, нет ли экспортируемой функции без аннотации типа возврата.

Практика

Начни сверху. Задачи идут от простого к сложному: вспомнить факт, применить к случаю, затем senior-уровень. Открой, попробуй, потом открой ответ.

вспомнитьприменитьуглубить0 из 5 завершено
Связанные уроки

Что-то непонятно?

Задай вопрос по этому уроку. Вопросы анонимны и попадают напрямую автору — урок станет лучше.

хоткеи развернуть
поиск
K
пред. пьеса
k
след. пьеса
j
тиры
t
это меню
?
sources2
expand
  1. 01
  2. 02

Trademarks belong to their respective owners. Editorial reference only.