Глава 3. Объектные типы, интерфейсы и type aliases

253 просмотров
0 лайков
0 в избранном

Объектный тип

Форму объекта можно описать прямо в аннотации:

const user: { name: string; age: number } = {
    name: "Анна",
    age: 28,
};

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

Интерфейсы

interface — именованное описание формы объекта:

interface User {
    id: number;
    name: string;
    age: number;
    email?: string;          // необязательное поле (может отсутствовать)
    readonly createdAt: string; // нельзя менять после создания
}

const user: User = {
    id: 1,
    name: "Анна",
    age: 28,
    createdAt: "2024-01-01",
};

// user.createdAt = "2025-01-01"; // Ошибка: readonly
console.log(user.email);           // string | undefined

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

Знак ? после имени поля означает, что оно может отсутствовать. TypeScript добавляет | undefined к его типу:

interface Article {
    title: string;
    subtitle?: string; // string | undefined
}

const a: Article = { title: "Привет" }; // OK, subtitle можно не указывать

readonly

Защищает от переназначения на уровне компилятора (в рантайме защиты нет, это не Object.freeze):

interface Point {
    readonly x: number;
    readonly y: number;
}

const p: Point = { x: 10, y: 20 };
// p.x = 30; // Ошибка

type alias

type создаёт псевдоним типа. Для объектов синтаксис похож на интерфейс:

type Point = {
    x: number;
    y: number;
};

const p: Point = { x: 10, y: 20 };

Но type гибче — описывает не только объекты:

// Объединение строк
type Status = "new" | "paid" | "done";

// Псевдоним для примитива
type UserId = number;

// Функциональный тип
type Callback = (value: string) => void;

// Кортеж
type Pair = [string, number];

interface vs type — что выбрать

Для описания объектов и классов чаще берут interface, для объединений, кортежей и функциональных типов — type. На практике оба взаимозаменяемы для объектов.

Возможностьinterfacetype
Объекты
Объединения (A | B)
Кортежи
Расширениеextendsпересечение &
Декларация-слияние✓ (один интерфейс можно объявить дважды)

Расширение интерфейсов

Один интерфейс может расширять другие — это удобный способ строить иерархии:

interface Animal {
    name: string;
    age: number;
}

interface Pet extends Animal {
    owner: string;
}

interface WildAnimal extends Animal {
    habitat: string;
}

const cat: Pet = { name: "Мурка", age: 4, owner: "Анна" };
const wolf: WildAnimal = { name: "Серый", age: 6, habitat: "лес" };

Пересечение через type и &:

type WithTimestamps = { createdAt: string; updatedAt: string };
type User = { id: number; name: string };

type UserWithTimestamps = User & WithTimestamps;

Индексные сигнатуры

Когда ключи заранее неизвестны — например, словарь или кэш:

interface Cache {
    [key: string]: string;
}

const cache: Cache = {};
cache["user:1"] = "Анна";
cache["user:2"] = "Борис";

// Смешанный объект: фиксированные поля + произвольные
interface ApiResponse {
    status: number;
    data: unknown;
    [key: string]: unknown; // любые дополнительные поля
}

Record вместо индексной сигнатуры

Часто удобнее использовать утилиту Record<K, V>:

type UsersById = Record<number, string>;

const users: UsersById = {
    1: "Анна",
    2: "Борис",
};

Readonly-объекты целиком

Чтобы сделать все свойства объекта неизменяемыми, используют Readonly<T> или readonly в типе:

interface Config {
    apiUrl: string;
    timeout: number;
}

const config: Readonly<Config> = {
    apiUrl: "https://api.example.com",
    timeout: 5000,
};

// config.timeout = 1000; // Ошибка: readonly

Кейс из реального проекта: модель пользователя

Типичный паттерн — отдельные интерфейсы для «создания» и «в БД»:

// Что присылает фронтенд при регистрации
interface CreateUserDTO {
    name: string;
    email: string;
    password: string;
}

// Что хранится в базе (id и timestamps проставляет сервер)
interface User extends CreateUserDTO {
    id: number;
    createdAt: string;
    updatedAt: string;
}

// Что отдаём наружу по API (без пароля)
interface UserPublic {
    id: number;
    name: string;
    email: string;
}

function createUser(dto: CreateUserDTO): User {
    return {
        ...dto,
        id: nextId(),
        createdAt: now(),
        updatedAt: now(),
    };
}

function toPublic(user: User): UserPublic {
    return { id: user.id, name: user.name, email: user.email };
}

Кейс: дискриминируемые объединения

Когда объект может принимать несколько форм, добавляют общее поле-признак. Это мощный приём для обработки событий и ошибок:

type ApiResult =
    | { status: "success"; data: User }
    | { status: "error"; message: string };

function handle(result: ApiResult): void {
    if (result.status === "success") {
        console.log(result.data.name); // TS знает, что data есть
    } else {
        console.log(result.message);   // TS знает, что message есть
    }
}

Типичные ошибки

1. Забыли, что необязательное поле — это | undefined

interface User {
    email?: string;
}

const u: User = {};
console.log(u.email.length);
//          ^^^^^^^^
// Ошибка: возможно, email — undefined

Нужно проверить или использовать ?.:

console.log(u.email?.length);

2. Изменение readonly в рантайме «в обход»

const config: Readonly<Config> = { apiUrl: "x", timeout: 1 };
(config as any).timeout = 2; // скомпилируется, но это потеря защиты

Не используйте as any, чтобы обходить ограничения — это обнуляет смысл TypeScript.

3. Лишние поля в объекте

interface User {
    id: number;
    name: string;
}

const u: User = { id: 1, name: "Анна", age: 28 };
//                                       ^^^
// Ошибка: "age" не существует в типе User

Практика

  1. Опишите интерфейс Book с полями title, author, необязательным year и readonly isbn.
  2. Создайте интерфейсы CreatePostDTO (то, что присылает пользователь) и Post (то же + id и createdAt).
  3. Опишите тип FormEvent как дискриминируемое объединение: { type: "submit"; data: FormData } или { type: "reset" }.
  4. Сделайте тип Dictionary = Record<string, number> и попробуйте добавить в него значения.

Итог

Мы научились описывать форму объектов через interface и type, использовать необязательные и readonly-поля, индексные сигнатуры и расширение интерфейсов. Разобрали реальный паттерн с DTO и публичной моделью, а также мощный приём — дискриминируемые объединения. В следующей главе — типизация функций.

Комментарии 0

Для добавления комментариев необходимо войти или зарегистрироваться.

Пока нет комментариев. Станьте первым!