Вывод типов и наведение: компилятор уже знает

компилятор≈ 9 минут

Здесь будет встроен видео-разбор темы (5–7 минут, YouTube / RuTube / VK Видео) — тем, кто лучше воспринимает на слух, текст ниже не обязателен.

В части I вы писали let count = 0 и про типы не думали — а тип у переменной был. Компилятор выводит его из значения сам: у каждой переменной, каждого поля и каждого возврата тип уже есть, просто его никто не печатал. Осталось научиться его видеть и понимать, где своя аннотация что-то обещает, а где вслух повторяет известное.

Разделы темы:·

Аннотация — не источник типа, а один из способов его сообщить. Основной способ другой: компилятор смотрит на значение, которым переменную инициализировали, и делает вывод сам. Аннотацию при этом можно не писать вовсе — тип от этого не исчезнет.

ts
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

Вывод случился на первой строке: 0 — число, значит count имеет тип number. Строка в него уже не лезет, и на третьей строке компилятор скажет: Тип "string" не может быть назначен для типа "number". А потом код всё равно выполнится и напечатает ноль — типы живут только до запуска, дальше едет обычный JavaScript.

Примитивами вывод не ограничивается. Массив объектных литералов компилятор разбирает до состава полей — и потом ловит опечатку в имени, которое никто нигде не объявлял.

ts
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

Ни одного типа мы не написали, а u.nmae подчёркнут: Свойство "nmae" не существует в типе "{ name: string; age: number; }". Заметьте: выведенный тип элемента компилятор процитировал прямо в тексте ошибки — приём из следующего раздела построен ровно на этом. Запуск подтверждает диагноз — два undefined, ровно та тихая ошибка, ради которой всё и затевалось.

Вывод — не догадка по имени переменной, а расчёт по значению. Переменная userName, которой присвоили 42, будет числом, и никакие подозрения к имени сюда не примешиваются.

В редакторе выведенный тип показывают наведением курсора: подводите мышь к имени — и во всплывающей подсказке появляется const user: { name: string; years: number; }. Это главный ежедневный инструмент, и если редактор понимает TypeScript, настраивать ничего не нужно.

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

ts
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

Диагностика на третьей строке печатает выведенный тип дословно, с точностью до порядка полей. Тип, которого мы не писали, оказывается вполне читаемым — его просто негде было увидеть.

Тот же приём показывает и сигнатуру функции. Возврат компилятор собирает сразу из всех веток.

ts
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

В ответе — (text: string) => number | null: параметр мы объявили сами, а возврат склеился из двух return. Про такие объединения будет отдельная глава; пока важно другое — компилятор составил тип сам, ничего не спрашивая.

У приёма одно слепое пятно: если значение и правда можно назначить в null, отказа не будет и тип останется невидимым. На такой случай есть тип never — в него не влезает вообще ничего.

ts
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

Вторая строка проходит молча, третья отвечает Тип "null" не может быть назначен для типа "never". С never приём не ослепнет никогда, просто null короче печатать.

Одно и то же значение выводится в разный тип в зависимости от того, чем объявлена переменная. Проверим приёмом из прошлого раздела.

ts
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

const переприсвоить нельзя — значит, значение больше никогда не изменится, и тип можно взять самый точный: литерал "left". У let впереди целая жизнь присваиваний, поэтому компилятор расширяет тип до string. Кавычки в диагностике при этом удваиваются — Тип ""left"": внешние ставит сам компилятор, внутренние принадлежат литеральному типу.

Разница не косметическая. Она решает, пустят ли значение в функцию, которая ждёт один из двух вариантов.

ts
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

fixed проходит, chosen — нет: Аргумент типа "string" нельзя назначить параметру типа ""left" | "right"". Значение в обеих переменных одинаковое, а типы разные, потому что тип отвечает не за сегодняшнее значение, а за все допустимые.

И тут же ловушка, на которой спотыкаются все. const перед объектом защищает саму переменную, а не её поля — присвоить config.dir что угодно никто не мешает, значит и тип поля расширяется до string.

ts
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

Лечится это конструкцией as const, которой отведена статья «readonly и as const: неизменяемость в типах» в главе про объекты. Пока хватит правила: точный литеральный тип живёт только в const-переменной с примитивом, а внутри объекта расширяется.

Расширение — единственное место, где на вывод влияет не значение, а способ объявления. Привычка «объявляем через const, пока не понадобилось иначе» получает второй смысл: типы выходят точнее.

Вывод идёт не только снизу вверх, от значения к переменной. Есть и встречное направление: тип приходит сверху, из того места, куда значение попадает. Это контекстная типизация, и вы её уже видели, просто не заметили.

ts
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

Стрелка внутри map — без аннотаций, и всё в порядке: map у массива чисел объявлен так, что его колбэк принимает number, оттуда компилятор тип и берёт. У функции double никакого «оттуда» нет — она объявлена сама по себе, и на четвёртой строке будет Параметр "n" неявно имеет тип "any".

Контекст исчезает, стоит вырвать колбэк из вызова в отдельную переменную. И появляется снова, если у переменной есть аннотация.

ts
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

У byAsc аннотация Compare — параметры a и b типизированы из неё, писать (a: number, b: number) было бы дублированием. У byDesc аннотации нет, и на четвёртой строке сразу две диагностики про неявный any, по одной на параметр.

Отсюда практичный вывод: аннотировать удобнее переменную-функцию целиком, а не каждый параметр по отдельности. Одно имя типа заменяет три аннотации и заодно сверяет возврат.

const price: number = 100 — аннотация, которая не добавляет ничего: компилятор вывел бы то же самое. Такие строки не вредны, но и не полезны — они молча устаревают при первой правке значения. Полезной аннотация становится там, где она что-то обещает.

Первое такое место — параметры: выводить их не из чего, без аннотации будет TS7006. Второе — возвращаемое значение. Здесь вывод справляется и сам, но есть разница, где обнаружится ошибка.

ts
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

Функция обещала цену, а вернула строку. Компилятор это заметил, но сказал у вызывающего — на четвёртой строке. В большом файле такое выглядит как «ошибка не там, где ошиблись»: чинить бегут переменную total, а виновата функция. Аннотация возврата переносит диагностику на место преступления.

ts
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

Тот же текст ошибки, но теперь он на второй строке — прямо на return. Аннотация не изменила ни поведения, ни выведенного типа: она изменила адрес претензии.

Третье место — контейнер, у которого в момент создания нечего осмотреть.

ts
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

Две диагностики про одно и то же. TS7034 на объявлении: Переменная "rows" неявным образом получает тип "any[]" в некоторых местах, где ее тип невозможно определить. И TS7005 на использовании: Переменная "rows" неявно имеет тип "any[]". Одна аннотация const rows: string[] = [] снимает обе. Здесь тип — не украшение, а единственный источник знания: из пустых скобок вывести нечего.

МестоАннотацияПочему
const price = 100не нужнавывод даёт ровно то же, а лишняя строка устареет при правке значения
Параметр функцииобязательнавыводить не из чего: без неё TS7006 и молчаливый any
Возврат функцииполезнаошибка находится в теле функции, а не у того, кто её вызвал
const rows = []обязательнав пустом литерале нечего осматривать — TS7034
Литерал, который должен остаться литераломосторожноconst dir: string стирает тип "left" до string

Аннотация не только сообщает тип — она его и ограничивает. Написав string там, где вывелся бы литерал, вы своими руками огрубляете знание, которое компилятор уже добыл.

ts
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

move(a) проходит, move(b) — нет, хотя значение в обеих переменных одно и то же. Это тот же механизм, что и с let в третьем разделе, только расширение здесь сделали не по невнимательности, а собственной рукой.

  • Тип есть у любой переменной, даже когда его никто не писал: аннотация тип только сообщает, а выводит его компилятор из значения при инициализации.
  • Посмотреть выведенный тип можно наведением в редакторе, а в песочнице — присвоив значение в null или never и прочитав отказ.
  • const с примитивом даёт точный литеральный тип, let расширяет его до string или number; поля объекта расширяются даже под const.
  • Контекстная типизация приносит тип сверху: у колбэка внутри вызова параметры типизированы, у той же функции в отдельной переменной — уже нет.
  • Аннотация полезна там, где она обещает: параметры, возврат (ошибка остаётся в функции) и пустые контейнеры. Дублировать очевидный вывод не нужно.

1/4Какой тип компилятор выведет для let side = "left"?

2/4Функция move принимает "left" | "right". Рядом объявлено const config = { dir: "left" }. Что скажет компилятор на move(config.dir)?

3/4Почему в nums.map((n) => n * 2) параметру n аннотация не нужна, а в отдельно объявленной function double(n) — нужна?

4/4Функция без аннотации возврата вернула строку, а её результат присвоили в переменную типа number. Где компилятор покажет ошибку?

Угадать, что вывел компилятор

Перед вами четыре объявления. Запишите свою догадку о выведенном типе каждого, подставьте её вместо null и запустите: если угадали — компилятор промолчит, если нет — сам напечатает правильный ответ.

ts
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

Компилятор ответит так:

tsлистинг · не запускается
Листинг только для чтения: этот код не запускается в браузере.

Главная ловушка — первые две строки. У title тип не string, а литерал "каталог": это const с примитивом. У counter — number, а не 0: let расширяет. Ответ string для title компилятор тоже примет молча — он не ошибочен, просто менее точен, и это стоит держать в голове: молчание означает «сюда влезает», а не «вы назвали ровно тот тип».

Для tags вывелся string[], а не кортеж из двух элементов, и для item — тип со всеми полями сразу, без литералов внутри.

Ошибка не там, где ошиблись

Компилятор жалуется на четвёртую строку, хотя виновата функция: String(...) здесь лишний. Сначала добавьте аннотацию возврата, чтобы диагностика переехала внутрь функции, а потом почините саму функцию.

ts
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

Шаг первый — аннотация : number. Текст ошибки тот же, но теперь она на строке с return, то есть указывает на настоящую причину:

tsлистинг · не запускается
Листинг только для чтения: этот код не запускается в браузере.

Шаг второй — убрать String:

tsлистинг · не запускается
Листинг только для чтения: этот код не запускается в браузере.

Аннотация возврата не изменила поведения ни на йоту — она изменила адрес претензии. Ровно за это её и пишут в функциях, которыми пользуется кто-то ещё.

Пустой массив

Код работает, но компилятор выдаёт две диагностики — TS7034 и TS7005. Исправьте одной строкой и объясните себе, почему их было две.

ts
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

Из [] выводить нечего, поэтому компилятор откладывает решение: массив «эволюционирует» по мере присваиваний, но только пока всё видно в одной области видимости. Здесь push происходит внутри функции, отследить это не получается — отсюда TS7034 на объявлении и TS7005 на использовании.

tsлистинг · не запускается
Листинг только для чтения: этот код не запускается в браузере.

Одна аннотация снимает обе диагностики: они говорили про одну и ту же неизвестность — в объявлении и в месте, где ею пользуются.