Урок 8: Типы объектов и псевдонимы типов (type)

В предыдущих уроках мы разобрали примитивные типы, массивы, кортежи и enum. Мы научились описывать данные, которые являются простыми и линейными. Но настоящие программирование, особенно веб-разработка, состоит из объектов. Пользователи, товары, посты в блоге, ответы от сервера, всё это сложные структуры, состоящие из множества свойств.

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

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

Описание формы объекта

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

Представьте, что мы работаем с данными пользователя. В JavaScript мы бы просто создали объект:

javascript
const user = {
  name: 'Максим',
  age: 30,
  isAdmin: true
};

В TypeScript мы можем создать переменную user и явно указать компилятору: «Эй, эта переменная является объектом у которого есть строка name, число age и булево isAdmin». Делается это с помощью аннотации типа после имени переменной.

typescript
const user: { name: string; age: number; isAdmin: boolean } = {
  name: 'Максим',
  age: 30,
  isAdmin: true
};

Обрати внимание на синтаксис. Мы описываем тип объекта в фигурных скобках {}. Внутри мы перечисляем свойства и их типы через точку с запятой name: string;. Это так называемый «объектный литерал» для типа.

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

typescript
// ОШИБКА: свойство "age" должно быть number, а не string
const user: { name: string; age: number; isAdmin: boolean } = {
  name: 'Мария',
  age: 'двадцать пять', // Type 'string' is not assignable to type 'number'
  isAdmin: false
};

// ОШИБКА: отсутствует обязательное свойство "isAdmin"
const anotherUser: { name: string; age: number; isAdmin: boolean } = {
  name: 'Анна',
  age: 25
  // Property 'isAdmin' is missing...
};

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

Необязательные свойства и readonly

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

Дополним наш тип пользователя.

typescript
const userWithOptional: {
  name: string;
  age: number;
  isAdmin: boolean;
  phoneNumber?: string; // Необязательное свойство
} = {
  name: 'Максим',
  age: 30,
  isAdmin: true
  // phoneNumber можно не указывать
};

Теперь мы можем смело создавать объекты как с полем phoneNumber, так и без него. Попытка же обратиться к необязательному свойству, которого может не существовать, потребует от нас проверки на undefined.

typescript
console.log(userWithOptional.phoneNumber); // Выведет undefined
// console.log(userWithOptional.phoneNumber.length); // Ошибка! Object is possibly 'undefined'

// Правильно: сначала проверяем
if (userWithOptional.phoneNumber) {
  console.log(userWithOptional.phoneNumber.length);
}

Еще один полезный модификатор, это readonly. Он указывает, что свойство должно быть assigned (присвоено) только один раз, при создании объекта и не может быть изменено в дальнейшем.

typescript
const immutableUser: {
  readonly name: string;
  readonly age: number;
} = {
  name: 'Максим',
  age: 30
};

immutableUser.name = 'Алексей'; // ОШИБКА: Cannot assign to 'name' because it is a read-only property.

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

Вложенные объекты

Объекты редко бывают плоскими. Часто они содержат другие объекты. Например, у пользователя может быть адрес, который сам является объектом с полями citystreet и zipCode.

Мы можем описать это прямо в аннотации типа.

typescript
const userWithAddress: {
  name: string;
  address: {
    city: string;
    street: string;
    zipCode: number;
  };
} = {
  name: 'Максим',
  address: {
    city: 'Москва',
    street: 'Арбат',
    zipCode: 119019
  }
};

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

typescript
// ОШИБКА в типе вложенного свойства
userWithAddress.address.zipCode = '119019'; // Type 'string' is not assignable to type 'number'

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

Создание псевдонимов с помощью ключевого слова type

Вот здесь мы подходим к самому вкусному, переиспользованию типов. TypeScript позволяет нам создавать псевдонимы (alias) для любых типов, будь то примитив, массив, кортеж или сложный объект. Для этого используется ключевое слово type.

Давайте решим проблему из предыдущего примера и вынесем тип пользователя в псевдоним.

typescript
// Создаем псевдоним типа User
type User = {
  name: string;
  age: number;
  isAdmin: boolean;
  phoneNumber?: string;
};

// Используем псевдоним везде, где нужен этот тип
const max: User = {
  name: 'Максим',
  age: 30,
  isAdmin: true
};

const anna: User = {
  name: 'Анна',
  age: 25,
  isAdmin: false,
  phoneNumber: '+79991234567'
};

Не правда ли, стало гораздо чище? Мы отделили описание формы данных от их использования. Теперь тип User стал единым источником истины для всего нашего кода.

  1. Удобство и читаемость: Код становится менее многословным и более понятным.

  2. Масштабируемость и поддержка: Если нам нужно добавить новое обязательное свойство (например, email), мы изменяем код всего в одном месте, в псевдониме User. TypeScript сразу же укажет на все места, где объект не соответствует новому описанию и мы быстро всё исправим. Без псевдонима нам пришлось бы вручную искать и править все аннотации по всему коду, что неизбежно привело бы к ошибкам.

Псевдонимы типов можно создавать для чего угодно.

typescript
// Для примитива
type ID = number;
// Для функции
type Callback = () => void;
// Для объединения (union type)
type Status = 'active' | 'inactive' | 'pending';

// А затем использовать их в других типах
type User = {
  id: ID;
  status: Status;
  onUpdate: Callback;
};

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

Объединение и пересечение типов

Псевдонимы типов открывают door to более продвинутым возможностям, таким как объединение (Union) и пересечение (Intersection) типов.

Объединение типов (Union Types |)

Иногда объект может быть одним из нескольких вариантов. Например, в системе есть два типа пользователей: обычный пользователь (BasicUser) и администратор (Admin). Они имеют разные наборы прав и, следовательно, разные свойства.

typescript
type BasicUser = {
  name: string;
  age: number;
  role: 'user'; // Литеральный тип
};

type Admin = {
  name: string;
  age: number;
  role: 'admin'; // Литеральный тип
  accessLevel: number;
};

// Объединение типов: User может быть либо BasicUser, либо Admin
type User = BasicUser | Admin;

const user1: User = {
  name: 'Иван',
  age: 25,
  role: 'user'
  // accessLevel здесь не нужен
};

const user2: User = {
  name: 'Марья',
  age: 35,
  role: 'admin',
  accessLevel: 5 // а здесь обязателен
};

В этом примере свойство role выступает в роли дискриминанта, оно позволяет TypeScript (и нам) понять, с каким именно типом объекта мы работаем в данный момент.

Пересечение типов (Intersection Types &)

Пересечение позволяет комбинировать несколько типов в один. Новый тип будет иметь все свойства всех исходных типов. Это полезно для расширения существующих типов.

typescript
type Person = {
  name: string;
  age: number;
};

type Employee = {
  employeeId: number;
  department: string;
};

// Создаем новый тип, объединяющий в себе и Person и Employee
type EmployedPerson = Person & Employee;

const max: EmployedPerson = {
  name: 'Максим',
  age: 30,
  employeeId: 12345,
  department: 'Development'
  // Должны быть указаны все свойства из Person и все из Employee
};

Это эквивалентно ручному описанию типа { name: string; age: number; employeeId: number; department: string; }, но гораздо удобнее, если мы хотим переиспользовать уже существующие типы.

Type Aliases или Interfaces

Наверняка ты уже слышал или столкнешься с другим способом описания форм объектов interface. На данном этапе можно считать, что type и interface очень похожи. Большую часть времени они взаимозаменяемы.

typescript
// С помощью interface
interface User {
  name: string;
  age: number;
}

// С помощью type
type User = {
  name: string;
  age: number;
};

Есть тонкие различия (например, interface можно расширять с помощью extends, а type с помощью &), но для начала можно использовать то, что тебе больше нравится. Я лично часто предпочитаю type за его универсальность (им можно описать не только объект, но и union, кортеж и т.д.). Мы подробнее разберем interface и их отличия от type в одном из следующих уроков.

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

Давай закрепим материал на практических задачах. Я настоятельно рекомендую открыть редактор кода (например, TypeScript Playground) и попробовать решить их самостоятельно.

Задача 1: Описание объекта книги
Создай псевдоним типа Book. Книга должна иметь следующие свойства:

  • title (строка, обязательное)

  • author (строка, обязательное)

  • year (число, обязательное)

  • genre (строка, необязательное)

  • rating (только число от 1 до 5, необязательное)

Создай несколько переменных типа Book.

Решение:

typescript
type Book = {
  title: string;
  author: string;
  year: number;
  genre?: string;
  rating?: 1 | 2 | 3 | 4 | 5; // Используем литеральный тип для ограничения значений
};

const book1: Book = {
  title: 'Преступление и наказание',
  author: 'Ф.М. Достоевский',
  year: 1866,
  genre: 'Роман',
  rating: 5
};

const book2: Book = {
  title: '1984',
  author: 'Джордж Оруэлл',
  year: 1949
  // genre и rating отсутствуют, что разрешено
};

Задача 2: Функция для работы с объектами
Напиши функцию getBookDescription(book: Book): string, которая принимает объект книги и возвращает строку описания, например: «Преступление и наказание by Ф.М. Достоевский (1866)».

Решение:

typescript
function getBookDescription(book: Book): string {
  return `${book.title} by ${book.author} (${book.year})`;
}

console.log(getBookDescription(book1)); // "Преступление и наказание by Ф.М. Достоевский (1866)"

Задача 3: Вложенные объекты и объединения
Опиши тип для профиля пользователя в социальной сети.

  • Пользователь должен иметь id (число), username (строка) и profile (объект).

  • Объект profile может быть двух типов:

    • PublicProfile: содержит displayName (строка) и bio (строка, необязательно).

    • PrivateProfile: содержит email (строка) и birthDate (строка, необязательно).

  • Создай функцию getDisplayName(profile: Profile), которая возвращает displayName для публичного профиля и email для приватного.

Решение:

typescript
type PublicProfile = {
  kind: 'public'; // Дискриминант
  displayName: string;
  bio?: string;
};

type PrivateProfile = {
  kind: 'private'; // Дискриминант
  email: string;
  birthDate?: string;
};

// Объединение типов
type Profile = PublicProfile | PrivateProfile;

type User = {
  id: number;
  username: string;
  profile: Profile;
};

// Создаем пользователей
const user1: User = {
  id: 1,
  username: 'max_ivanov',
  profile: {
    kind: 'public',
    displayName: 'Максим Иванов',
    bio: 'Разработчик из Москвы'
  }
};

const user2: User = {
  id: 2,
  username: 'anna_private',
  profile: {
    kind: 'private',
    email: 'anna@mail.com'
  }
};

// Функция использует сужение типа на основе дискриминанта `kind`
function getDisplayName(profile: Profile): string {
  if (profile.kind === 'public') {
    // TypeScript знает, что здесь profile имеет тип PublicProfile
    return profile.displayName;
  } else {
    // TypeScript знает, что здесь profile имеет тип PrivateProfile
    return profile.email;
  }
}

console.log(getUserDisplayName(user1.profile)); // "Максим Иванов"
console.log(getUserDisplayName(user2.profile)); // "anna@mail.com"

Задача 4 (продвинутая): Пересечение для композиции
Создай тип Coordinates с полями lat (широта, число) и lng (долгота, число).
Создай тип City, который включает в себя все свойства из базового типа Place (name: stringcountry: string) и добавляет свойства из Coordinates.

Решение:

typescript
type Coordinates = {
  lat: number;
  lng: number;
};

type Place = {
  name: string;
  country: string;
};

// Используем пересечение для создания нового типа
type City = Place & Coordinates;

const moscow: City = {
  name: 'Москва',
  country: 'Россия',
  lat: 55.7558,
  lng: 37.6173
};

В следующем уроке мы продолжим углубляться в тему типов и поговорим об интерфейсах (interface), узнаем их сходства и различия с typeи выясним, когда что лучше использовать.

Полный курс с уроками по TypeScript для начинающих:
https://max-gabov.ru/typescript-dlya-nachinaushih

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

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

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