Учебник веб-разработки
Разделы учебника
На этой странице

II–III. JS, TypeScript и модели данных

TypeScript: типы и границы проверки

Оглавление · Значения и функции JS

Задача: описать вход и результат преобразования так, чтобы редактор и компилятор находили ошибки до запуска. Нужны функции, объекты и массивы JS. Аннотация типа относится к программе; она не проверяет содержимое HTTP-ответа.

Тип как множество допустимых значений

Для рассуждения удобно считать string множеством строк, а литеральный тип "news" — множеством из одной строки. Объектный тип задаёт необходимые поля:

type LessonEvent = {
  title: string;
  location: string;
};

function label(event: LessonEvent): string {
  return `${event.title} — ${event.location}`;
}

const event = { title: "Открытие", location: "Площадь" };
const text = label(event); // результат выведен как string

type вводит имя типа. Двоеточие после параметра или функции задаёт аннотацию. Значение event имеет подходящую структуру, поэтому отдельный конструктор LessonEvent не нужен. В проекте включён strict в базовой конфигурации.

Модель множества полезна, но система TypeScript не является доказательством корректности всех исполнений. Приведения, any, изменяемые ссылки и внешние данные позволяют обойти проверки. Объектный тип также обычно допускает дополнительные поля: это структурная совместимость, а не точная форма JSON.

const withId = { title: "Открытие", location: "Площадь", id: "e1" };
label(withId); // совместимая структура
// label({ title: "Открытие" }); // ошибка: нет location

Для свежих объектных литералов действуют дополнительные проверки лишних полей. Ни одна из них не удаляет поле во время выполнения. Типы объектов и аннотации.

Сужение типа

Объединение string | null допускает строку или отсутствие. Пока вариант не известен, нельзя обращаться с ним как со строкой.

function caption(title: string | null): string {
  if (title === null) return "Без названия";
  return title.trim(); // здесь title: string
}

Проверка условия уточняет множество возможных значений в этой ветке. Это narrowing — сужение типа. Проверка на null сохраняет пустую строку; if (!title) объединила бы её с отсутствием. Для свойства title?: string нужно учитывать также undefined. Сужение типов.

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

function normalizedTitle(value: unknown): string | null {
  if (typeof value !== "string") return null;
  const title = value.trim();
  return title.length > 0 ? title : null;
}

value as string — обещание разработчика компилятору. Оно не вызывает String(value), не проверяет значение и не заменяет разбор входа. Та же граница у ! после выражения: утверждение отсутствия null не создаёт значение, которого нет.

Generics сохраняют связь входа и выхода

function first<T>(items: readonly T[]): T | undefined {
  return items[0];
}
const title = first(["Открытие", "Мастерская"]); // string | undefined
const count = first([1, 2]); // number | undefined

T — параметр типа, выбранный для вызова. Вход и результат используют один параметр: связь не теряется, как у функции, принимающей any[]. undefined нужен из-за пустого массива. Generic сам по себе не проверяет вход и не сообщает ничего о бизнес-смысле элемента. Параметры типов.

readonly T[] запрещает изменять массив через этот параметр в проверяемом коде. Он не замораживает объект во время выполнения и не делает вложенные поля неизменяемыми. Если другой владелец сохранил изменяемую ссылку, он всё ещё может изменить тот же массив.

Проверка типов и выполнение — разные пути

Исходник схемы
flowchart LR
  Source["Исходник TypeScript"] --> Check["Typecheck: диагностика"]
  Source --> Build["Преобразование и сборка"]
  Build --> JS["JavaScript"]
  JS --> Run["Выполнение с реальными данными"]
  External["HTTP / CMS / пользователь"] --> Run

Стрелки обозначают этапы и входы. Typecheck анализирует исходник; проверка новых внешних значений нужна уже во время выполнения. В Next преобразование исходника и проверка типов — разные ответственности. Наличие типа NewsItem не подтверждает, что CMS прислала корректную новость.

Где это видно в Atmanki

В контрактах тип EventItem выводится из runtime-схемы через z.infer. В расписании параметр — readonly EventItem[], а результат — ScheduleDay[] из UIKit. Приложение преобразует бизнес-данные в данные отображения, не заставляя UIKit импортировать CMS-контракт.

CMS-адаптер сначала работает с unknown, затем нормализует и разбирает значение схемой. Record<string, unknown> допускает неизвестные поля; он не доказывает, что documentId — строка. Полноценный разбор рассмотрен в главе о моделях и валидации.

Практика и самопроверка

Создайте в своей учебной ветке временный apps/docs/src/lesson.ts с типом LessonEvent и функцией titlesAt из прошлой главы. Укажите readonly LessonEvent[] на входе и string[] на выходе. Запустите из корня pnpm --filter @atmanki/docs typecheck.

По очереди внесите три ошибки: событие без location, вызов со строкой вместо массива, events.push(...) внутри функции. Каждая должна вызвать диагностику. Верните исправный вариант. Добавьте first и проверьте обработку пустого массива. Удалите временный файл после упражнения.

Объясните, почему приведённый через as LessonEvent HTTP-ответ может сломать label во время выполнения даже при успешном typecheck.