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.