Почему TypeScript исчезает после сборки?

mr. Cooper • 21 час назад • Веб-разработка
Почему TypeScript исчезает после сборки?

Есть странный момент, который почти неизбежно замечает разработчик после первого серьёзного проекта на TypeScript.

В исходниках вокруг кода полно типов:

//ts

interface User {
  id: number;
  name: string;
}

function getUser(user: User): string {
  return user.name;
}

А потом открываешь собранный JavaScript:

//js

function getUser(user) {
  return user.name;
}

И от User ничего не осталось.

Нет interface, нет : string, нет : User. Даже generics, если они использовались, тоже исчезли.

Возникает вполне естественный вопрос: куда делся TypeScript и зачем тогда мы вообще писали все эти типы?

Ответ связан с тем, что TypeScript и JavaScript решают разные задачи.

TypeScript нужен до запуска программы

TypeScript не является отдельным runtime-языком, который должен продолжать работать внутри браузера вместе с JavaScript.

Его основная задача - проверить исходный код до того, как программа начнёт выполняться.

Например:

//ts

function sum(a: number, b: number): number {
  return a + b;
}

sum("10", 20);

TypeScript сразу покажет ошибку.

Он видит, что функция ожидает два числа, а вместо первого аргумента передана строка.

После исправления код можно превратить в обычный JavaScript:

//js

function sum(a, b) {
  return a + b;
}

В готовом приложении уже нет необходимости хранить информацию о том, что a и b когда-то были объявлены как number.

Эта информация уже выполнила свою работу.

Получается довольно простая схема:

TypeScript проверяет → сборщик преобразует → JavaScript выполняется.

Поэтому исчезновение типов - не побочный эффект сборки. Это нормальный результат работы TypeScript.

Почему interface просто пропадает

Особенно хорошо это видно на интерфейсах.

//ts

interface Product {
  id: number;
  title: string;
  price: number;
}

После компиляции от него может не остаться вообще ничего.

Почему?

Потому что interface не создаёт объект.

Он не хранит данные.

Он не выполняет функцию.

Он только описывает TypeScript, какой формы должен быть объект.

Когда проверка закончена, JavaScript такой информации не требуется.

Сам объект при этом никуда не исчезает:

//ts

const product: Product = {
  id: 10,
  title: "Keyboard",
  price: 5000
};

превратится примерно в:

//js

const product = {
  id: 10,
  title: "Keyboard",
  price: 5000
};

Объект остался.

Исчезло только описание его типа.

С type происходит то же самое

Например:

//ts

type UserId = string | number;

const id: UserId = 42;

После сборки:

//js

const id = 42;

UserId не превращается в какой-то специальный JavaScript-объект.

Это всего лишь инструкция для системы типов.

Та же история с объединениями:

//ts

type Status = "loading" | "success" | "error";

После компиляции такого типа тоже нет.

Но значение:

//ts

const status = "success";

остаётся.

И это важное различие: значения нужны программе, типы нужны разработчику и компилятору.

А generics куда деваются?

С generics происходит похожая вещь.

//ts

function first<T>(items: T[]): T {
  return items[0];
}

TypeScript использует T, чтобы проверить связь между входными и выходными данными.

Например:

//ts

const number = first([1, 2, 3]);
const text = first(["one", "two"]);

В первом случае TypeScript понимает, что результат - number.

Во втором - string.

Но JavaScript во время выполнения не обязан хранить информацию о T.

После компиляции получится:

//js

function first(items) {
  return items[0];
}

На этом этапе конкретный тип уже определяется самим значением и поведением JavaScript.

Здесь часто возникает неправильное ожидание

Можно подумать:

Если TypeScript проверил, что переменная имеет тип User, значит программа гарантированно получит настоящий User.

Вот здесь TypeScript уже не может дать такую гарантию.

Особенно если данные приходят извне.

Например:

//ts

const response = await fetch("/api/user");
const user: User = await response.json();

На первый взгляд всё выглядит хорошо.

Мы объявили:

//ts

interface User {
  id: number;
  name: string;
}

и сказали TypeScript, что user имеет этот тип.

Но TypeScript не отправляется на сервер, чтобы проверить JSON.

Если API по ошибке вернёт:

//json

{
  "id": "hello",
  "name": 123
}

TypeScript не остановит этот ответ.

Он не выполняется в момент получения JSON.

После сборки интерфейса User вообще не существует.

Именно поэтому между типом и проверкой реальных данных есть большая разница.

TypeScript не проверяет мир вокруг программы

Это, пожалуй, главный момент во всей истории.

TypeScript отлично знает о коде, который находится перед ним.

Он может проверить:

//ts

function saveUser(user: User) {
  // ...
}

Он может сообщить, что где-то передали неправильный объект.

Но он не знает заранее, что реально окажется в:

  • ответе API;

  • localStorage;

  • JSON-файле;

  • данных формы;

  • параметрах URL;

  • стороннем сервисе;

  • сообщении от другого приложения.

Все эти значения появляются во время выполнения.

А TypeScript к этому моменту уже закончил свою работу.

Поэтому иногда нужен runtime-валидатор

Если приложение получает данные из внешнего источника, одной статической типизации может быть недостаточно.

Допустим, сервер должен вернуть:

//ts

interface User {
  id: number;
  name: string;
}

Можно дополнительно описать схему:

//ts

const userSchema = z.object({
  id: z.number(),
  name: z.string()
});

И уже после получения данных проверить их:

//ts

const data = await response.json();
const user = userSchema.parse(data);

Здесь происходят две разные проверки.

TypeScript проверяет сам код во время разработки.

Валидатор проверяет реальные данные во время выполнения.

И это не дублирование одной и той же работы.

У них разные задачи.

Почему тогда не оставить типы в JavaScript?

Потому что JavaScript просто не знает, что с ними делать.

Представим:

//ts

interface User {
  id: number;
  name: string;
}

Что должен делать браузер с этим интерфейсом?

Проверять каждый объект?

Выбрасывать ошибку при неправильном значении?

Хранить метаданные о каждом поле?

Отправлять эту информацию в DevTools?

TypeScript сам по себе ничего такого не требует.

Его идея гораздо проще: обнаружить проблему как можно раньше, пока разработчик ещё пишет код.

Если ошибку можно найти в редакторе, нет особого смысла заставлять браузер обнаруживать её уже после запуска.

Но не всё из TypeScript исчезает

Есть важная оговорка.

Фраза «TypeScript полностью исчезает после сборки» технически не совсем правильна.

TypeScript умеет компилировать конструкции, которые влияют на итоговый JavaScript.

Например, некоторые возможности языка создают реальный runtime-код.

Условно говоря, если конструкция должна существовать во время выполнения, компилятору приходится превратить её в JavaScript.

А вот:

//ts

interface User {
  id: number;
}

существовать в runtime не обязано.

Поэтому оно и исчезает.

Главный вопрос здесь не в том, «является ли код написанным на TypeScript».

Вопрос в другом:

нужна ли эта конструкция программе после запуска?

Если нужна - она должна каким-то образом попасть в JavaScript.

Если она использовалась только для проверки типов - её можно удалить.

Получается довольно интересный парадокс

Чем лучше TypeScript выполняет свою работу, тем меньше его видно в production-коде.

Разработчик пишет:

//ts

function createUser(
  name: string,
  age: number
): User {
  // ...
}

Получает подсказки редактора, проверку аргументов, поиск несовместимых типов и ошибки ещё до запуска.

А затем браузер получает гораздо более простой JavaScript:

//js

function createUser(name, age) {
  // ...
}

И это не означает, что TypeScript был бесполезен.

Наоборот.

Его задача закончилась раньше.

Можно провести простую границу.

TypeScript отвечает за то, чтобы разработчик не отправил в программу очевидно неправильный код.

Runtime-проверки отвечают за то, чтобы приложение не доверяло без проверки данным, которые пришли извне.

Когда эти две вещи смешивают, появляются странные ожидания от типизации.

Когда их разделяют, поведение TypeScript становится намного понятнее.

Типы исчезают после сборки не потому, что компилятор их потерял.

Они исчезают потому, что JavaScript они больше не нужны.

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

Комментарии

Пока нет комментариев. Будьте первым, кто напишет.

Чтобы оставить комментарий, войдите в аккаунт.

Похожие статьи