← Курс/Псевдонимы типов (type aliases)#143 из 257+25 XP

Псевдонимы типов (type aliases)

Ключевое слово type

type создаёт псевдоним — новое имя для существующего типа или описывает сложный тип:

// Простой псевдоним примитива
type UserId = number
type Email = string

// Объектный тип
type User = {
  id: UserId
  name: string
  email: Email
  age: number
}

const user: User = { id: 1, name: 'Алексей', email: 'alex@mail.ru', age: 30 }

Опциональные и readonly поля

type Profile = {
  readonly id: number    // нельзя изменить после создания
  name: string
  age?: number           // опциональное — может быть undefined
  bio?: string           // опциональное
}

const profile: Profile = { id: 1, name: 'Алексей' }
profile.age = 30         // OK
profile.id = 2           // Ошибка: Cannot assign to 'id' because it is a read-only property

Union типы

Union позволяет переменной иметь один из нескольких типов:

// Строковый union — только указанные значения
type Status = 'active' | 'inactive' | 'banned'
type Direction = 'left' | 'right' | 'up' | 'down'

// Union примитивов
type Id = string | number  // строка или число

// Работа с union — TypeScript требует проверки:
function formatId(id: Id): string {
  if (typeof id === 'string') {
    return id.toUpperCase()  // TypeScript знает что id — string
  }
  return id.toString()       // TypeScript знает что id — number
}

Intersection типы

Intersection объединяет несколько типов в один (объект должен иметь все поля):

type User = { name: string; email: string }
type Admin = { role: 'admin'; permissions: string[] }

type AdminUser = User & Admin
// AdminUser = { name: string; email: string; role: 'admin'; permissions: string[] }

const admin: AdminUser = {
  name: 'Мария',
  email: 'maria@mail.ru',
  role: 'admin',
  permissions: ['read', 'write', 'delete'],
}

type vs interface

Оба описывают форму объекта. Ключевые различия:

// type — закрытый, но поддерживает union и mapped types
type Status = 'active' | 'inactive'     // union — только через type
type UserKeys = keyof User              // mapped type — только через type

// interface — открытый, поддерживает declaration merging
interface Animal { name: string }
interface Animal { age: number }  // OK! Расширяет предыдущий
// Animal теперь { name: string; age: number }

// type нельзя расширить повторным объявлением:
type Animal = { name: string }
type Animal = { age: number }  // Ошибка: Duplicate identifier 'Animal'

**Правило**: используй interface для объектов и классов, type для union, intersection и примитивных псевдонимов.

Практический пример

type Priority = 1 | 2 | 3
type TaskStatus = 'todo' | 'in-progress' | 'done'

type Task = {
  readonly id: number
  title: string
  status: TaskStatus
  priority: Priority
  assignee?: string      // опциональный
}

function createTask(title: string, priority: Priority): Task {
  return {
    id: Date.now(),
    title,
    status: 'todo',
    priority,
  }
}

function updateStatus(task: Task, status: TaskStatus): Task {
  return { ...task, status }  // не мутируем, возвращаем новый объект
}

Примеры

Система управления пользователями с runtime-валидацией объектных типов, union значений и intersection

// В TypeScript типы проверяются при компиляции.
// Реализуем эквивалентную runtime-валидацию.

// Симуляция union type: Status = 'active' | 'inactive' | 'banned'
const VALID_STATUSES = ['active', 'inactive', 'banned']

// Симуляция union type: Role = 'viewer' | 'editor' | 'admin'
const VALID_ROLES = ['viewer', 'editor', 'admin']

// Симуляция readonly + optional: создаёт User объект с валидацией
function createUser(name, email, role = 'viewer') {
  if (typeof name !== 'string' || name.trim() === '') {
    throw new TypeError('name должен быть непустой строкой')
  }
  if (typeof email !== 'string' || !email.includes('@')) {
    throw new TypeError('email должен содержать @')
  }
  if (!VALID_ROLES.includes(role)) {
    throw new TypeError(`role должен быть одним из: ${VALID_ROLES.join(', ')}`)
  }

  // Object.freeze симулирует readonly поля
  return Object.freeze({
    id: Date.now(),
    name: name.trim(),
    email: email.toLowerCase(),
    role,
    status: 'active',
    createdAt: new Date().toISOString(),
  })
}

// Симуляция intersection: AdminUser = User & AdminRights
function promoteToAdmin(user, permissions) {
  if (!Array.isArray(permissions)) {
    throw new TypeError('permissions должен быть массивом')
  }
  // Возвращаем новый объект = User & AdminRights (intersection)
  return Object.freeze({
    ...user,
    role: 'admin',
    permissions: Object.freeze([...permissions]),
    promotedAt: new Date().toISOString(),
  })
}

// Симуляция функции принимающей union type: Status
function updateStatus(user, status) {
  if (!VALID_STATUSES.includes(status)) {
    throw new TypeError(`Некорректный статус. Доступны: ${VALID_STATUSES.join(', ')}`)
  }
  return Object.freeze({ ...user, status })
}

// Форматирует ID: Id = string | number
function formatId(id) {
  if (typeof id === 'string') return id.toUpperCase()
  if (typeof id === 'number') return `#${id.toString().padStart(6, '0')}`
  throw new TypeError('id должен быть string или number')
}

// --- Демонстрация ---
console.log('=== Создание пользователей ===')
const alice = createUser('Алиса', 'alice@mail.ru')
const bob = createUser('Боб', 'bob@mail.ru', 'editor')
console.log(alice)
console.log(bob)

console.log('\n=== Intersection: промоция в admin ===')
const adminAlice = promoteToAdmin(alice, ['read', 'write', 'delete'])
console.log(adminAlice)

console.log('\n=== Union: обновление статуса ===')
const bannedBob = updateStatus(bob, 'banned')
console.log(`${bannedBob.name}: ${bannedBob.status}`)

try {
  updateStatus(alice, 'suspended')  // TypeError — не в union
} catch (e) {
  console.log(e.message)
}

console.log('\n=== Union type Id: string | number ===')
console.log(formatId('usr_abc'))   // 'USR_ABC'
console.log(formatId(42))          // '#000042'

console.log('\n=== Readonly — нельзя мутировать ===')
try {
  alice.name = 'Другое имя'  // В strict mode: TypeError
} catch (e) {
  console.log('Попытка изменить readonly:', e.message)
}
console.log('alice.name:', alice.name)  // Алиса — не изменилось