Система типов

Что это такое

TypeScript описывает форму значения: какие у него свойства, что в них лежит, что функция принимает и что возвращает. Описания сравнивает компилятор и там, где они расходятся, сообщает об этом. Система структурная: два типа совместимы, если у них одни и те же члены, независимо от названий.

Почему это важно

Насколько система типов полезна, зависит от того, сколько она выводит сама, без подсказок. Кодовая база, где почти всё приходится аннотировать руками, — это вторая копия программы, которую тоже надо поддерживать, и со временем она расходится с оригиналом. Если же большинство типов выведено, остаётся несколько небольших объявлений на границах — параметры функций, экспортируемые формы, — а всё внутри проверяется от них. Где проходят эти границы — вот в чём весь навык, и от этого зависит, будет TypeScript полезным инструментом или лишней работой без выгоды.

Ключевые идеи

Типы структурные, а не номинальные

Объект совместим с типом, если у него есть все требуемые члены совместимых типов, а объявлять, что класс реализует интерфейс, не нужно: значение этого класса удовлетворяет ему и так. Два типа с одинаковой формой, объявленные порознь, взаимозаменяемы.

type Point = { x: number; y: number };

const origin = { x: 0, y: 0, label: "O" };
const p: Point = origin;   // нормально — лишние свойства здесь допустимы

Исключение — проверка лишних свойств: когда объектный литерал присваивают прямо типизированной переменной, неизвестные свойства действительно отмечаются, и происходит это именно потому, что опечатка обычно случается как раз там.

Пусть вывод делает работу

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

function totalCents(items: CartItem[]): number {
  return items.reduce((sum, item) => sum + item.priceCents * item.quantity, 0);
}

Для объединений берите type, для расширения — interface

Оба описывают форму объекта. interface можно открыть заново и слить с другим объявлением — для дополнения библиотек это удобно, во всех остальных случаях опасно; type же составляет объединения и пересечения, слиянию не поддаётся, поэтому для прикладного кода это более безопасный выбор по умолчанию.

Моделируйте отсутствие явно

strict: true делает null и undefined обычными типами, а не чем-то, что прячется за каждым значением. Один этот флаг превращает «cannot read property of undefined» из падения во время выполнения в ошибку компиляции, поэтому включать его стоит первым.

type User = { id: string; name: string; deletedAt: string | null };

any отключает проверку для значения

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

Практика

Типизируйте границу, остальное выводите

Возьмите небольшой модуль на JavaScript, который получает данные и отрисовывает их. Аннотируйте только возвращаемый тип функции загрузки и параметр функции отрисовки, затем включите strict, запустите компилятор и исправьте то, что он сообщит, — больше нигде аннотаций не добавляйте.

Готово, когда: модуль компилируется со strict, ни одна локальная переменная не несёт явной аннотации, и компилятор ловит намеренную опечатку, которую вы вносите в отрисовку.

Замените три any настоящим типом

Найдите в кодовой базе три употребления any, выпишите форму, которая на самом деле есть у каждого значения, и замените их типом или unknown.

Готово, когда: ни одно из трёх не компилируется без проверки, доказывающей форму, и хотя бы один скрытый дефект выходит наружу.

Частые ошибки

  • Аннотировать всё подряд — аннотации превращаются во вторую программу, которая расходится с первой; исправить: аннотируйте параметры и экспортируемые возвраты, а локальные переменные оставьте выводу.
  • Оставлять strict выключенным — null проскакивает мимо всех проверок, и ошибки во время выполнения остаются; исправить: включите strict и исправляйте найденные ошибки по одному файлу за раз.
  • Гасить ошибку через any — проверка перестаёт помогать для этого значения и всего производного от него; исправить: используйте unknown и сужайте либо опишите настоящую форму.
  • Тянуться к as, чтобы подогнать тип — приведение отменяет решение компилятора, не меняя значение во время выполнения; исправить: сделайте значение действительно подходящим или проверьте его на границе.
  • Типизировать функцию по её реализации — тогда вызывающий код зависит от внутренних деталей, и каждый рефакторинг его ломает; исправить: описывайте, что функция обещает, а не как она это делает сейчас.

Что читать дальше

  • JavaScript (02-frontend/06-javascript) — семантика значений, которые вы описываете; типы не меняют ни поведение this, ни приведение типов.
  • Сужение типов и объединения (02-frontend/19-typescript/02-narrowing-and-unions) — там, где система типов начинает предотвращать состояния, а не только описывать их.
  • Чистый код (01-cs-fundamentals/08-clean-code) — ясная сигнатура преследует ту же цель, но достигает её без компилятора.