TypeScript / 39 минут

Интерфейсы и optional-свойства

Описывайте форму объектов и значения по умолчанию.

В процессе Теория: впереди Практика: впереди Тест: впереди
03 / 12
После этой главы
  • Описывайте форму объектов и значения по умолчанию.
  • Применить тему в задаче «Интерфейсы и optional-свойства» и объяснить граничные случаи.

Около 9 минут на чтение и разбор примеров. Практика — отдельно, в своём темпе.

Описание объектного контракта

Интерфейс задаёт форму объекта: имена свойств, их типы и методы. Он не создаёт объект во время исполнения и не содержит автоматическую runtime-валидацию. Его роль — позволить компилятору проверить, что код правильно использует договорённую структуру. Для внешнего JSON понадобится отдельный валидатор, даже если интерфейс выглядит очень подробным.

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

TypeScript
interface Product {
  title: string;
  price: number;
  discount?: number;
}
const product: Product = { title: 'Учебник', price: 500 };

Необязательное поле и undefined

Знак ? разрешает отсутствие свойства. При чтении price и discount имеют разные гарантии: price должен существовать по контракту, discount может оказаться undefined. Прежде чем использовать скидку в арифметике, нужно выбрать правило отсутствия. Если это ноль, оператор ?? явно выражает нужный выбор и сохраняет законную нулевую скидку.

TypeScript
function finalPrice(product: Product): number {
  return product.price - (product.discount ?? 0);
}
console.log(finalPrice({ title: 'JS', price: 100 })); // 100

Отсутствующее поле и поле со значением undefined близки при обычном чтении, но не полностью одинаковы во всех операциях и настройках TypeScript. exactOptionalPropertyTypes позволяет строже моделировать допустимость явной записи undefined. В любом случае не считайте optional разрешением передавать null: его нужно отдельно включить в тип, если он является допустимым значением.

readonly и поверхностное ограничение

readonly запрещает присваивание свойству через данное статическое представление. Он не делает объект физически замороженным и не превращает каждое вложенное значение в неизменяемое. Другая изменяемая ссылка может по-прежнему менять тот же runtime-объект. Поэтому readonly описывает дисциплину использования, а не абсолютную границу безопасности.

TypeScript
interface Settings {
  readonly id: string;
  nested: { page: number };
}
const settings: Settings = { id: 'main', nested: { page: 1 } };
settings.nested.page = 2; // вложенный объект не объявлен readonly
// settings.id = 'other'; // ошибка проверки

Для сложной модели состояния полезно выбрать, где именно разрешены изменения и кто создаёт новую версию данных. Бесконечные утверждения типов ради обхода readonly обычно означают, что контракт не соответствует реальной ответственности кода. Иногда нужен изменяемый builder, а наружу — только readonly-представление готового результата.

Расширение, пересечения и лишние свойства

interface может расширять другой интерфейс через extends. Type alias удобен для союзов, пересечений и других форм типов. Пересечение A & B требует одновременного соответствия обеим частям; несовместимые поля не превращаются в свободный выбор. Для вариантов нужен union, а не intersection.

TypeScript
interface Identified { id: string }
interface User extends Identified { name: string }
type PublicUser = User & { readonly active: boolean };

У свежих объектных литералов есть проверки лишних полей, которые помогают находить опечатки. Но присваивание подходящего объекта переменной интерфейсного типа не удаляет его дополнительные runtime-поля. Это особенно важно для публичного ответа: тип PublicUser не вырезает passwordHash из настоящего объекта. Нужен явный выбор полей при построении DTO.

Словари и индексы

Индексная сигнатура или Record описывает набор динамических ключей. Но даже словарь не гарантирует, что любой возможный ключ реально присутствует. Настройка noUncheckedIndexedAccess помогает выражать такое отсутствие при чтении. Для данных с заранее известными обязательными свойствами лучше перечислить их явно, чем делать весь контракт безграничным словарём.

TypeScript
type Counters = Record<string, number>;
const counts: Counters = { javascript: 3 };
const current = counts['typescript'] ?? 0;

Разделяйте тип ключа и допустимые значения. Если ключ может быть только 'js' или 'ts', закрытый union лучше передаёт намерение, чем string. Если вход приходит от пользователя, runtime-проверка должна подтвердить, что строка действительно относится к разрешённому набору.

Практика необязательной скидки

Нужно вернуть price - discount, а отсутствие discount считать нулём. Применяйте запасное значение к скидке, а не к результату вычитания целиком. В ином случае корректный нулевой итог может быть случайно заменён. Цена и скидка по условию уже числа; преобразование произвольного JSON в эту форму находится вне данной функции.

Проверьте скидку ноль, отсутствие скидки и скидку, равную цене. После решения расширьте контракт readonly-полями и убедитесь, что функция остаётся чистой. Затем попробуйте передать null как discount и прочитайте ошибку проверки: тип не включает null, даже если объект визуально похож на ожидаемый.

Загружаем редактор…
Следующая темаТипизация функций

Музыка: Reverie — Scott Buckley · CC BY 4.0