Зачем нужны типы, по-настоящему
Типы TypeScript — это доказательство о форме рантайм-значений на этапе компиляции, которое полностью стирается до запуска; ошибка типа — это контрпример, найденный компилятором раньше ваших пользователей, а не рантайм-проверка.
Вы переименовываете функцию: getUser(id: string) превращается в getUser(id: string, opts: { fresh: boolean }). По коду разбросаны 23 места вызова. В мире чистого JavaScript вы делаете grep, чините найденное, выкатываете — а через три недели забытый вызов где-нибудь в обработке ошибок падает с Cannot read properties of undefined. В TypeScript же в момент изменения сигнатуры компилятор подсвечивает красным все 23 устаревших вызова ещё до того, как вы запустите программу. Это не услужливое автодополнение. Это машина, статически доказывающая, что ваше изменение неполное.
К концу этого урока вы будете точно знать, почему такое доказательство работает, почему оно исчезает в рантайме и где в нём дыры — чтобы больше не воспринимать зелёную компиляцию как гарантию.
Типы — это доказательство, а не подсказка
Поверхностная подача TypeScript — «автодополнение и красные волнистые линии». Это занижает суть. Аннотация типа — это утверждение о каждом значении, которое когда-либо пройдёт через данную позицию в рантайме, а проверяющий типы — это движок доказательств, который либо подтверждает, что утверждение верно на каждом пути, либо выдаёт вам конкретный контрпример: ту самую строку, где может прийти значение неправильной формы.
function totalCents(items: { priceCents: number }[]): number {
return items.reduce((sum, it) => sum + it.priceCents, 0);
}
totalCents([{ priceCents: 100 }, { priceCents: 250 }]);
// возвращает 350
totalCents([{ price: 100 }]);
// Error: Object literal may only specify known properties, and
// 'price' does not exist in type '{ priceCents: number; }'.Второй вызов никогда не выполнится. Проверяющий отверг его, потому что форма значения противоречит контракту функции. Вы не писали тест на тему «а что если кто-то передаст price вместо priceCents» — система типов покрыла этот случай и любое другое несовпадение формы исчерпывающе.
Выигрыш при рефакторинге: каждый устаревший вызов загорается
Вот где типы окупаются на реальной кодовой базе. Меняете контракт — и проверяющий перечисляет каждое место, которое больше ему не удовлетворяет.
// до
function track(event: string) { /* ... */ }
track("checkout");
// после: делаете второй аргумент обязательным
function track(event: string, props: Record<string, unknown>) { /* ... */ }
track("checkout");
// Error: Expected 2 arguments, but got 1.
// An argument for 'props' was not provided.В JavaScript это тихая мина — props оказывается undefined, и баг всплывает только на той ветке кода, которая его читает, возможно уже в проде. В TypeScript неполный рефакторинг — это ошибка компиляции в каждом пропущенном вызове одновременно. Проверяющий выполняет поиск по всей программе, который иначе пришлось бы делать руками через grep.
Типы стираются: их нет до запуска кода
Вот факт, который переосмысляет всё. Типы TypeScript имеют нулевое представление в рантайме. Компилятор их проверяет, а затем вырезает. Тот JavaScript, который реально выполняется, и не подозревает, что типы когда-либо существовали.
// input.ts
function greet(name: string): string {
return `Hello, ${name}`;
}
const x: number = 41 + 1;// эмитированный output.js — аннотаций больше нет
function greet(name) {
return `Hello, ${name}`;
}
const x = 41 + 1;: string, : number, аннотация возврата — всё стёрто. Это стирание типов (type erasure), и у него есть жёсткое следствие: типы не могут валидировать рантайм-данные. Тип ограничивает только те значения, которые компилятор видит на этапе сборки. В тот момент, когда значение пересекает границу, невидимую компилятору — ответ fetch(), JSON.parse, тело формы, process.env — тип становится предположением, а не гарантией.
const res = await fetch("/api/user");
const user: { id: string; name: string } = await res.json();
// ^? const user: { id: string; name: string }
user.name.toUpperCase();
// Компилируется нормально. Но res.json() имеет тип any (Promise<any>),
// так что эта аннотация — ОБЕЩАНИЕ, данное вами компилятору, а не факт.
// Если сервер вернёт { id: 7, name: null }, это упадёт в рантайме.Аннотация говорит «поверь мне, форма такая». Компилятор вам верит. Если данные по сети не согласны, тип не дал вам ничего. Реальные системы закрывают этот разрыв рантайм-валидатором на границе — и именно об этом материал про zod в уроке 08: заново установить в рантайме тот контракт, который система типов предполагала статически.
▸Почему это работает
Почему компилятор может стереть типы и при этом оставаться полезным? Потому что корректность здесь — это свойство этапа сборки над замкнутой программой. Внутри кода, который TypeScript видит, каждое присваивание, возврат и вызов проверены против объявленных типов, поэтому в этом замкнутом мире доказательство держится. Стирание безопасно именно потому что доказательство уже было закрыто на этапе компиляции — рантайму не нужны типы, чтобы быть корректным; они были нужны только пока работал проверяющий. Подвох в том, что замкнутый мир заканчивается на каждой границе ввода-вывода, где внутрь просачиваются непроверенные данные с типом any.
Честная цена: any, структурные лазейки и неполнота гарантий
Зелёная компиляция — это не доказательство корректности. TypeScript намеренно градуальный и unsound (несостоятельный) в конкретных, задокументированных местах, чтобы его можно было внедрять постепенно и моделировать реальный JavaScript.
const data: any = JSON.parse(localStorage.getItem("cart")!);
data.foo.bar.baz(); // нет ошибки — `any` отключает любую проверку
data.totalCents.toFixed(2); // нет ошибки — и может взорваться в рантаймеany — это дыра в доказательстве. Любое значение, проходящее через any, освобождено от проверки и может вернуться обратно в типизированный код как ложь. Другие задокументированные лазейки, дающие зелёную компиляцию над кодом, который всё равно может упасть:
const el = document.querySelector(".btn") as HTMLButtonElement;
// ^ assertion: вы переопределяете проверяющего
el.disabled = true; // упадёт, если элемент не найден (на деле el равен null)
const arr: number[] = [1, 2, 3];
const n = arr[10];
// ^? number <-- ЛОЖЬ. n равен `undefined` в рантайме (без noUncheckedIndexedAccess)Это не баги TypeScript; это цена системы типов, достаточно прагматичной, чтобы обернуть 30-летний динамический язык. Когда вы видите в чужом коде any, as или непроверенный доступ по индексу — воспринимайте это как место, где у доказательства намеренная брешь: именно здесь стоит заложить рантайм-проверку.
Ваша функция аннотирует ответ fetch как `{ id: string }`, компилируется чисто, но в проде `id` иногда число, и код падает. Что на самом деле произошло?
Расположите по порядку, что происходит с типизированным значением: от написания до возможного сбоя в рантайме на плохих данных I/O.
- 1 Вы пишете значение плюс аннотацию типа в исходнике .ts
- 2 tsc проверяет значение против объявленного типа на этапе сборки
- 3 tsc стирает аннотации и эмитит обычный .js
- 4 Непроверенное значение fetch/parse входит через границу I/O в рантайме и может нарушить стёртый контракт
- 01Почему «типы — это просто автодополнение» — неверная ментальная модель, и какая верна?
- 02Что такое стирание типов и каково его самое важное следствие?
- 03Почему чистая компиляция — не доказательство корректности?
Прочная модель: типы TypeScript — это статическое доказательство о форме рантайм-значений, закрываемое проверяющим на этапе сборки и затем стираемое — эмитированный JavaScript не несёт никакой информации о типах вообще. Поэтому типы превосходно ловят неполные рефакторинги (все устаревшие вызовы загораются разом), но бессильны против непроверенных данных ввода-вывода, и поэтому зелёная компиляция не является доказательством корректности, пока any, assertions и доступ по индексу остаются открытыми дырами. Теперь, когда вы видите ответ fetch с аннотацией конкретного типа и чистой компиляцией, первый вопрос, который вы задаёте: «Есть ли здесь рантайм-валидатор на границе, или это просто обещание, данное компилятору?»
Практика
Начни сверху. Задачи идут от простого к сложному: вспомнить факт, применить к случаю, затем senior-уровень. Открой, попробуй, потом открой ответ.
Что-то непонятно?
Задай вопрос по этому уроку. Вопросы анонимны и попадают напрямую автору — урок станет лучше.