Основы вывода: как проверщик заполняет типы, которые вы не написали
TypeScript выводит большинство типов, чтобы их не приходилось писать. Правила — типизация по инициализатору, расширение при const против let, контекстная типизация колбэков, наилучший общий тип массивов — предсказуемы, стоит их увидеть. Аннотируйте границы, выводите локальные.
Вы добавляете : 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`)?
Параметр колбэка, получающий тип от функции, в которую он передан — без какой-либо аннотации — типизируется ____ типизацией.
- 01Объясните расширение при const против let и почему свойства объекта расширяются, даже когда связывание — const.
- 02Что такое контекстная типизация, где она применяется и когда перестаёт работать?
- 03Какова практическая политика аннотаций — что аннотировать, а что доверить выводу и почему?
Вывод — причина, почему TypeScript не мешает вам: формы, которые вы научились разбирать структурно в прошлом уроке, по большей части заполняются за вас, а тяжёлую работу делают расширение и контекстная типизация. Правила расширения, которые вы только что увидели — const, хранящий "a", let, расширяющий до string, расширение свойств, as const как лазейка — это ровно та механика, на которой строится следующий урок о литеральных типах. Но сперва заглянем в три особых типа — any, unknown и never — потому что понимание того, как вывод и совместимость ведут себя на вершине и дне решётки типов, и удерживает any от тихого отравления всего, что вывод вам только что дал. Теперь, когда видите комментарий ревьюера «избыточная аннотация», проверьте, не покрывает ли вывод этот слот уже — а когда tsc тормозит, посмотрите, нет ли экспортируемой функции без аннотации типа возврата.
Практика
Начни сверху. Задачи идут от простого к сложному: вспомнить факт, применить к случаю, затем senior-уровень. Открой, попробуй, потом открой ответ.
Что-то непонятно?
Задай вопрос по этому уроку. Вопросы анонимны и попадают напрямую автору — урок станет лучше.