Learning Book

Объектные типы: интерфейсы, type alias, классы

Два способа описать объект

TypeScript предлагает два ключевых слова для описания типов объектов: interface и type. Оба делают почти одно и то же, но с важными различиями.

interface

Описывает форму объекта. Может быть расширен через extends:

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

interface Admin extends User {
  permissions: string[]
}

const admin: Admin = {
  id: 1,
  name: 'Алекс',
  email: 'alex@example.com',
  permissions: ['users.manage', 'content.edit']
}

type alias

Создаёт псевдоним для любого типа — не только объектного:

type User = {
  id: number
  name: string
  email: string
}

// type может описывать то, что interface не может:
type ID = string | number                      // union
type Pair<T> = [T, T]                          // кортеж
type Callback = (data: string) => void         // функция
type Keys = keyof User                         // "id" | "name" | "email"

interface vs type — когда что

// interface — для объектов, которые будут расширяться
interface Animal {
  name: string
}

interface Dog extends Animal {
  breed: string
}

// type — для union-ов, кортежей, mapped types, computed types
type Result = Success | Failure
type Coordinates = [number, number]
type Readonly<T> = { readonly [K in keyof T]: T[K] }

Ключевые различия

Возможностьinterfacetype
Описание объекта
extendsЧерез &
Union / Intersection
Кортежи
Mapped types
Declaration merging
Скорость компиляции (extends)Быстрее& медленнее

Declaration merging

Interface с одинаковым именем автоматически объединяются. Это полезно для расширения библиотечных типов:

// Расширяем Window из lib.dom.d.ts
interface Window {
  analytics: AnalyticsClient
}

// Теперь window.analytics типизирован
window.analytics.track('click')

Внимание: Declaration merging может быть непредсказуемым в приложениях — два файла могут случайно определить один и тот же интерфейс, и они молча объединятся. Для приложений (не библиотек) предпочитайте type.

Рекомендация

Используйте type по умолчанию — он более предсказуемый и гибкий. Переходите на interface, когда нужно:

  • extends для наследования объектов (быстрее для компилятора)
  • Declaration merging (расширение библиотечных типов)

Объединение (|) и пересечение (&)

Union type (|) — «или»

Значение может быть одним из нескольких типов:

type StringOrNumber = string | number

function format(value: StringOrNumber): string {
  if (typeof value === 'string') return value.toUpperCase()
  return value.toFixed(2)
}

Union — это объединение множеств: string | number содержит все строки и все числа.

Intersection type (&) — «и»

Значение должно удовлетворять всем типам одновременно:

type HasName = { name: string }
type HasAge = { age: number }
type Person = HasName & HasAge

const person: Person = { name: 'Алекс', age: 25 } // Нужны ОБА поля

Intersection — это пересечение множеств: объект, который принадлежит обоим типам.

Заметка: Для примитивов intersection часто даёт never: string & number — невозможно быть одновременно строкой и числом.

Модификаторы свойств

readonly — только чтение

type Config = {
  readonly host: string
  readonly port: number
}

const config: Config = { host: 'localhost', port: 3000 }
config.host = 'example.com' // ❌ Ошибка: Cannot assign to 'host'

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

type User = {
  id: number
  name: string
  bio?: string // Может быть string или undefined
}

const user: User = { id: 1, name: 'Алекс' } // ✅ — bio необязателен

Типизация функций

// Стрелочная нотация
type Greet = (name: string) => string

// С перегрузками (только interface)
interface Formatter {
  (value: string): string
  (value: number): string
  (value: Date): string
}

// Функция с опциональными и default параметрами
function createUser(
  name: string,
  age: number = 25,
  email?: string
): User {
  return { id: Date.now(), name, age, email }
}

Массивы и кортежи

// Массив — переменная длина, один тип элементов
const numbers: number[] = [1, 2, 3]
const names: Array<string> = ['Алекс', 'Кира'] // Альтернативный синтаксис

// Кортеж — фиксированная длина, типы по позициям
const point: [number, number] = [10, 20]
const entry: [string, number] = ['age', 25]

// Именованные кортежи (для читаемости)
type RGB = [red: number, green: number, blue: number]

Index signatures

Для объектов с динамическими ключами:

type Dictionary = {
  [key: string]: number
}

const scores: Dictionary = {
  math: 95,
  physics: 88,
  // Любой строковый ключ с числовым значением
}

Внимание: Index signature не проверяет, существует ли ключ: scores['nonexistent'] вернёт undefined, но тип будет number. Включите noUncheckedIndexedAccess в tsconfig для безопасности.

Utility types — встроенные трансформации

TypeScript предлагает набор utility types для трансформации существующих типов:

interface User {
  id: number
  name: string
  email: string
  age: number
}

// Все поля опциональные
type PartialUser = Partial<User>
// { id?: number; name?: string; email?: string; age?: number }

// Все поля обязательные
type RequiredUser = Required<PartialUser>

// Только выбранные поля
type UserPreview = Pick<User, 'id' | 'name'>
// { id: number; name: string }

// Все поля кроме указанных
type UserWithoutEmail = Omit<User, 'email'>
// { id: number; name: string; age: number }

// Запись с указанным типом ключей и значений
type UserRoles = Record<string, 'admin' | 'user' | 'guest'>

// Только для чтения
type FrozenUser = Readonly<User>

Комбинирование utility types

// Форма обновления пользователя: все поля кроме id, опциональные
type UpdateUser = Partial<Omit<User, 'id'>>
// { name?: string; email?: string; age?: number }
Копай глубже: Как устроены utility types внутри

Utility types реализованы через mapped types и conditional types. Например, Partial:

type Partial<T> = {
  [K in keyof T]?: T[K]
}

keyof T — union всех ключей типа T. [K in ...] — mapped type, перебирающий ключи. ? — делает свойство опциональным. T[K] — тип свойства K в T (indexed access type).

Класс как тип

В TypeScript класс одновременно создаёт значение (конструктор) и тип (форму экземпляра):

class Point {
  constructor(
    public x: number,
    public y: number
  ) {}

  distanceTo(other: Point): number {
    return Math.sqrt((this.x - other.x) ** 2 + (this.y - other.y) ** 2)
  }
}

// Point как тип — описывает форму экземпляра
function logPoint(p: Point) {
  console.log(`(${p.x}, ${p.y})`)
}

// Структурная типизация: объект-литерал совместим с классом!
logPoint({ x: 1, y: 2, distanceTo: () => 0 }) // ✅

Итого

КонцепцияКогда использовать
typeПо умолчанию: union-ы, кортежи, mapped types
interfaceРасширяемые объекты, declaration merging
| (union)Значение одного из нескольких типов
& (intersection)Значение, удовлетворяющее всем типам
readonly, ?Защита от мутации, опциональность
Utility typesТрансформация существующих типов