Урок 10: Опциональные свойства и свойства только для чтения

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

В предыдущих уроках мы с вами научились описывать форму объектов с помощью интерфейсов и псевдонимов типов. Но зачастую реальный мир данных не так строг: некоторые поля могут отсутствовать в одних объектах и присутствовать в других. А некоторые поля, однажды созданные, не должны меняться. Именно для этих задач TypeScript предлагает элегантные решения, которые мы сегодня и изучим.

Синтаксис ? для опциональных свойств

Давайте представим себе очень распространенную ситуацию. Мы разрабатываем приложение для социальной сети и описываем интерфейс для объекта User.

typescript
interface User {
  id: number;
  username: string;
  email: string;
  birthday: Date; // День рождения
}

Кажется, всё логично. Но что если пользователь только что зарегистрировался и еще не указал свою дату рождения? Или, возможно, он просто не хочет ей делиться? В соответствии с нашим текущим интерфейсом TypeScript будет ругаться на любой объект пользователя, у которого нет поля birthday, ведь мы объявили его обязательным.

В таких случаях нам на помощь приходят опциональные свойства. Они помечаются вопросительным знаком ? прямо в объявлении интерфейса или типа. Это простое действие сообщает TypeScript: «Эй, это свойство может существовать, а может и нет. И это нормально!».

Давайте перепишем наш интерфейс:

typescript
interface User {
  id: number;
  username: string;
  email: string;
  birthday?: Date; // День рождения теперь опционален
}

Теперь мы можем создавать объекты как с полем birthday, так и без него и TypeScript будет совершенно спокоен.

typescript
// Всё в порядке, у нас есть все обязательные поля
const newUser: User = {
  id: 1,
  username: 'max_ts',
  email: 'max@example.com'
};

// И это тоже корректно
const oldUser: User = {
  id: 2,
  username: 'anna_ts',
  email: 'anna@example.com',
  birthday: new Date(1990, 5, 15)
};

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

Но с большой силой приходит и большая ответственность. Когда мы работаем с опциональным свойством, мы должны всегда помнить, что его может не быть. Попытка обратиться к нему напрямую может привести к классической ошибке undefined.

typescript
console.log(newUser.birthday.getFullYear()); // Ошибка! Cannot read properties of undefined (reading 'getFullYear')

Поэтому перед использованием опционального свойства всегда необходимо проверять, существует ли оно. Это можно сделать с помощью простого if или используя современный оператор опциональной цепочки (?.), о котором мы подробнее поговорим в одном из будущих уроков.

typescript
// Безопасная проверка с помощью if
if (newUser.birthday) {
  console.log('Год рождения:', newUser.birthday.getFullYear());
} else {
  console.log('День рождения не указан');
}

// Или с использованием опциональной цепочки (более современный и краткий способ)
console.log('Год рождения:', newUser.birthday?.getFullYear());

Опциональные свойства можно использовать не только в интерфейсах, но и в псевдонимах типов (type) и даже при деструктуризации объектов, чтобы указать, что параметры функции являются необязательными.

Ключевое слово readonly

Теперь давайте поговорим о другой стороне медали, о неизменяемости. В программировании существует концепция иммутабельности (неизменяемости), которая помогает избегать множества ошибок, особенно в сложных приложениях. Если объект или его часть не должны изменяться после создания, лучше всего явно запретить эти изменения. Именно для этого в TypeScript существует ключевое слово readonly.

Свойство, помеченное как readonly, можно присвоить только при создании объекта. Любые последующие попытки его изменения приведут к ошибке на этапе компиляции.

Давайте вернемся к нашему примеру с пользователем. Поле id это уникальный идентификатор, который присваивается пользователю один раз при регистрации и никогда не должен меняться. Это идеальный кандидат для readonly.

typescript
interface User {
  readonly id: number; // ID пользователя нельзя изменить после создания
  username: string;
  email: string;
  birthday?: Date;
}

Теперь TypeScript зорко следит за тем, чтобы мы не попытались перезаписать id по ошибке.

typescript
const user: User = {
  id: 123,
  username: 'max_ts',
  email: 'max@example.com'
};

user.username = 'new_max_ts'; // Это разрешено, username не readonly
user.id = 456; // ОШИБКА! Cannot assign to 'id' because it is a read-only property.

Этот механизм обеспечивает дополнительный уровень безопасности вашего кода. Он защищает критические данные от случайных изменений и делает ваши намерения очевидными для других разработчиков, которые читают ваш код. Глядя на интерфейс, сразу понятно, что id это неизменяемый идентификатор.

Важно понимать, что readonly это проверка на этапе компиляции. Это не является гарантией неизменяемости во время выполнения JavaScript-кода. Однако для подавляющего большинства проектов этой защиты на этапе разработки более чем достаточно.

Ключевое слово readonly можно применять не только к свойствам интерфейсов, но и к отдельным переменным или константам, хотя для них обычно используют const. Разница заключается в области применения: const используется для переменных, а readonly для свойств объектов.

Сочетание readonly и опциональных свойств

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

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

typescript
interface User {
  readonly id: number;
  username: string;
  email: string;
  birthday?: Date;
  readonly promoCode?: string; // Опциональный и только для чтения
}

// При создании пользователя можно указать промокод...
const userWithPromo: User = {
  id: 124,
  username: 'alice_ts',
  email: 'alice@example.com',
  promoCode: 'WELCOME2023'
};

// ... но потом его нельзя ни изменить...
userWithPromo.promoCode = 'NEWYEAR2023'; // ОШИБКА!

// ... ни удалить (установить в undefined явно тоже не выйдет)
userWithPromo.promoCode = undefined; // ОШИБКА!

Обратите внимание на последний пример: даже попытка явно присвоить undefined опциональному readonly свойству приведет к ошибке. После первоначального присвоения значение фиксируется навсегда, независимо от того, было ли это значение string или undefined.

Практические примеры и задачи

Давайте закрепим теорию на практике. Я подготовил несколько задач и примеров, которые помогут вам увереннее чувствовать себя с опциональными и readonly свойствами.

Задача 1: Исправь ошибку

Перед тобой код, который почему-то не компилируется. Найди и исправь все ошибки, связанные с опциональными и readonly свойствами.

typescript
interface Book {
  readonly isbn: string;
  title: string;
  author: string;
  publicationYear?: number;
  genre?: string;
}

const myBook: Book = {
  isbn: '1234567890',
  title: 'TypeScript for Beginners',
  author: 'Max Gabov'
};

myBook.isbn = '0987654321'; // Попытка изменить ISBN
myBook.genre = 'Programming'; // Попытка добавить жанр после создания

function updatePublicationYear(book: Book, year: number): void {
  book.publicationYear = year;
}

updatePublicationYear(myBook, 2023);

Решение:
В коде есть две ошибки.

  1. Попытка изменить свойство isbn, которое помечено как readonly. Это запрещено.

  2. Попытка добавить свойство genre после создания объекта. В интерфейсе оно помечено как опциональное, это значит, что его можно указать при создании объекта, но не добавить позже (если только это не разрешено другими механизмами, например, индексными сигнатурами, о которых мы поговорим позже). Правильнее было бы указать genre сразу при создании myBook.

Исправленный код:

typescript
const myBook: Book = {
  isbn: '1234567890',
  title: 'TypeScript for Beginners',
  author: 'Max Gabov',
  genre: 'Programming' // Добавляем жанр при создании
};

// myBook.isbn = '0987654321'; // Эту строку нужно удалить, так как изменение запрещено.
// myBook.genre = 'Programming'; // И эту тоже удаляем, т.к. genre уже добавлен при создании.

// Функция updatePublicationYear работает корректно, так как publicationYear не является readonly.
updatePublicationYear(myBook, 2023);

Задача 2: Создай интерфейс

Опиши интерфейс Config для настроек приложения со следующими полями:

  • apiUrl (строка, обязательное, только для чтения)

  • timeout (число, опциональное, по умолчанию должно быть 5000, если не указано)

  • retry (логическое значение, опциональное)

  • version (строка, только для чтения, опциональное)

Решение:

typescript
interface AppConfig {
  readonly apiUrl: string;
  timeout?: number;
  retry?: boolean;
  readonly version?: string;
}

// Пример использования
const config: AppConfig = {
  apiUrl: 'https://api.myapp.com',
  timeout: 10000,
  // retry не указан, значит он может быть undefined
  version: '1.0.0'
};

// config.apiUrl = 'new-url'; // Запрещено!
config.timeout = 15000; // Разрешено, так как timeout не readonly.
// config.version = '2.0.0'; // Запрещено!

Задача 3: Функция для обновления профиля

Напиши функцию updateUserProfile, которая принимает текущий объект пользователя (User, мы используем наш интерфейс с idusernameemail и опциональным birthday) и объект с обновлениями (partialUpdate). Важное условие: функция должна возвращать новый объект пользователя, а не изменять исходный. В объекте обновлений можно менять только usernameemail и birthdayid менять нельзя.

Решение:
Эта задача проверяет понимание того, что readonly запрещает изменение только для конкретного свойства, но мы можем создать совершенно новый объект, куда скопируем старые данные и новые. Для обновлений мы можем использовать отдельный тип.

typescript
interface User {
  readonly id: number;
  username: string;
  email: string;
  birthday?: Date;
}

// Тип для обновлений. Все поля, кроме id, делаем опциональными.
type UserUpdate = Pick<User, 'username' | 'email' | 'birthday'>;

function updateUserProfile(currentUser: User, update: UserUpdate): User {
  // Возвращаем全新的ный объект, распространяя старые свойства и новые.
  return {
    ...currentUser,
    ...update
  };
}

const currentUser: User = {
  id: 101,
  username: 'old_username',
  email: 'old@email.com'
};

const updatedUser = updateUserProfile(currentUser, {
  username: 'new_username',
  email: 'new@email.com'
});

console.log(updatedUser);
// { id: 101, username: 'new_username', email: 'new@email.com' }
console.log(updatedUser.id === currentUser.id); // true, id остался тем же самым числом

Заключение

Десятый урок успешно пройден. Сегодня мы с вами освоили два фундаментальных концепта TypeScript, которые делают нашу систему типов гораздо более выразительной и надежной.

  1. Опциональные свойства (?) позволяют нам accurately описать объекты, которые могут иметь некоторые свойства, а могут и не иметь. Это краеугольный камень работы с неполными данными.

  2. Свойства только для чтения (readonly) помогают нам обеспечивать неизменяемость критически важных частей наших объектов, предотвращая случайные мутации и делая код более предсказуемым.

Сочетая эти два инструмента, мы можем создавать сложные и точные описания данных, с которыми работает наше приложение. Это не просто «прихоть» TypeScript, это реальные механизмы, которые помогают избегать целых классов ошибок еще до запуска кода, на этапе компиляции.

В следующих уроках мы затронем еще более интересную и мощную тему: типы объединений (Union Types) и систему литеральных типов (Literal Types), которые откроют нам новые горизонты для написания гибкого и строго типизированного кода.

Если вы хотите погрузиться в изучение TypeScript основательно и последовательно, посмотрите всю программу полного курса по TypeScript для начинающих. Там вас ждут все 40 уроков, начиная от основ и заканчивая продвинутыми темами.

Поделиться статьей:
Поддержать автора блога

Поддержка автора осуществляется с помощью специальной формы ниже, предоставленной сервисом «ЮMoney». Все платёжные операции выполняются на защищённой странице сервиса, что обеспечивает их корректность и полную безопасность.

Персональные рекомендации
Оставить комментарий