Объектный тип
Форму объекта можно описать прямо в аннотации:
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. На практике
оба взаимозаменяемы для объектов.
| Возможность | interface | type |
|---|---|---|
| Объекты | ✓ | ✓ |
| Объединения (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
Практика
- Опишите интерфейс
Bookс полямиtitle,author, необязательнымyearи readonlyisbn. - Создайте интерфейсы
CreatePostDTO(то, что присылает пользователь) иPost(то же +idиcreatedAt). - Опишите тип
FormEventкак дискриминируемое объединение:{ type: "submit"; data: FormData }или{ type: "reset" }. - Сделайте тип
Dictionary = Record<string, number>и попробуйте добавить в него значения.
Итог
Мы научились описывать форму объектов через interface и
type, использовать необязательные и readonly-поля,
индексные сигнатуры и расширение интерфейсов. Разобрали реальный паттерн с
DTO и публичной моделью, а также мощный приём — дискриминируемые объединения.
В следующей главе — типизация функций.
Комментарии 0
Пока нет комментариев. Станьте первым!