Типизация объектов

Типизация объектов в TypeScript — описание формы объекта: обязательных и опциональных свойств, их типов, модификаторов (readonly), index signatures — через inline-тип, type alias или interface.

Зачем нужно

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

Где используется

  • Модели данных: User, Product, Order
  • Параметры функций и возвращаемые значения
  • Конфигурационные объекты
  • Словари и маппинги

Основной контент

Inline-типизация объекта

// Прямо в аннотации переменной или параметра
const user: { id: string; name: string; email: string } = {
  id: "u-1",
  name: "Alice",
  email: "alice@example.com",
};

function greet(person: { name: string; age?: number }): string {
  const age = person.age !== undefined ? `, ${person.age}` : "";
  return `Hello, ${person.name}${age}!`;
}

Опциональные (?) и readonly свойства

interface Config {
  host: string;              // обязательное
  port?: number;             // опциональное: number | undefined
  readonly debug: boolean;   // только чтение
  readonly id: string;
}

const config: Config = { host: "localhost", debug: false, id: "c-1" };
// config.debug = true; // Error — readonly
// config.port;         // number | undefined — нужна проверка

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

interface Address {
  street: string;
  city: string;
  country: string;
  zip?: string;
}

interface User {
  id: string;
  name: string;
  address: Address;
  contacts?: {
    phone?: string;
    telegram?: string;
  };
}

const alice: User = {
  id: "u-1",
  name: "Alice",
  address: {
    street: "Ленина 1",
    city: "Москва",
    country: "Россия",
  },
};

console.log(alice.address.city);         // string
console.log(alice.contacts?.phone);      // string | undefined

Index Signatures — динамические ключи

// Объект с произвольными string-ключами
interface StringMap {
  [key: string]: string;
}

const translations: StringMap = {
  hello: "привет",
  goodbye: "пока",
};

// Смешанный: конкретные + динамические
interface FlexibleRecord {
  id: string;           // конкретный ключ
  [extra: string]: string; // динамические — ДОЛЖНЫ быть совместимы с id
}

Record для словарей

// Вместо index signature часто удобнее Record
type UserMap = Record<string, User>;        // string → User
type ScoreMap = Record<string, number>;     // string → number

type Role = "admin" | "user" | "guest";
type PermissionMap = Record<Role, string>; // конечный набор ключей

const perms: PermissionMap = {
  admin: ["read", "write", "delete"],
  user:  ["read", "write"],
  guest: ["read"],
};

Destructuring с типами

// TypeScript выводит типы при деструктурировании
const { id, name, address: { city } } = alice;
// id: string, name: string, city: string

// Деструктурирование с ренеймингом
const { id: userId, name: userName } = alice;
// userId: string, userName: string

// С дефолтными значениями
function greet({ name, age = 0 }: { name: string; age?: number }) {
  return `${name}, ${age}`;
}

Excess property check

interface Point { x: number; y: number }

// Избыточные свойства при прямом присваивании — ошибка
// const p: Point = { x: 0, y: 0, z: 0 }; // Error — z лишнее

// Но через промежуточную переменную — OK (duck typing)
const point3D = { x: 0, y: 0, z: 0 };
const p: Point = point3D; // OK — { x, y, z } структурно совместим с { x, y }

Частые ошибки

  • Обращаться к опциональному полю без проверкиconfig.port.toFixed() — ошибка если port не указан.
  • Смешивать index signature с несовместимыми конкретными полями{ id: string; [k: string]: number } — ошибка, id: string не совместим с number.
  • Ожидать строгую типизацию при промежуточной переменной — excess property check работает только при прямом присваивании.
  • Мутировать readonly поля — TypeScript запретит на этапе компиляции, но Object.freeze нужен для рантайм-защиты.

Связанные темы

Ресурсы