Интерфейсы

Interface — способ описания структуры объекта в TypeScript. Поддерживает наследование, реализацию в классах и declaration merging.

Зачем нужно

  • Описание контрактов: какие свойства и методы должен иметь объект
  • Наследование через extends — расширение существующих типов
  • Реализация в классах через implements — гарантия контракта
  • Declaration merging — расширение типов из сторонних библиотек

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

  • Описание структуры объектов, пропсов компонентов, API-ответов
  • Контракты для классов (implements)
  • Расширение типов сторонних библиотек
  • Описание формы данных в приложении

Предпосылки

Базовый синтаксис

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

// Объект должен соответствовать структуре
const user: User = {
  id: 1,
  name: "Alice",
  email: "alice@example.com",
};

// Ошибки:
const bad: User = {
  id: 1,
  name: "Alice",
  // Ошибка! Свойство 'email' отсутствует
};

const bad2: User = {
  id: 1,
  name: "Alice",
  email: "a@b.com",
  role: "admin", // Ошибка! 'role' не существует в типе User
};

Свойства интерфейса

interface Product {
  // Обязательное свойство
  id: number;

  // Опциональное свойство
  description?: string; // string | undefined

  // Только для чтения
  readonly createdAt: Date;

  // Метод (два синтаксиса — равнозначны)
  getPrice: number;
  getPrice:  => number;
}

const product: Product = {
  id: 1,
  createdAt: new Date,
  getPrice {
    return 99.99;
  },
};

product.id = 2;          // OK
product.createdAt = new Date(); // Ошибка! readonly

Наследование (extends)

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

interface Pet extends Animal {
  owner: string;
}

// Pet имеет: name, age, owner
const cat: Pet = {
  name: "Whiskers",
  age: 3,
  owner: "Alice",
};

// Множественное наследование
interface HasId {
  id: number;
}

interface HasTimestamps {
  createdAt: Date;
  updatedAt: Date;
}

interface Entity extends HasId, HasTimestamps {
  name: string;
}

// Entity: { id, createdAt, updatedAt, name }

Переопределение свойств при наследовании

interface Base {
  value: string | number;
  id: number;
}

interface Derived extends Base {
  value: string; // OK — сужение типа (string — подтип string | number)
  // id: string; // Ошибка! string не совместим с number
}

Реализация в классах (implements)

interface Serializable {
  serialize: string;
  deserialize(data: string): void;
}

interface Loggable {
  log(message: string): void;
}

// Класс реализует интерфейс(ы)
class User implements Serializable, Loggable {
  constructor(public name: string, public age: number) {}

  serialize: string {
    return JSON.stringify({ name: this.name, age: this.age });
  }

  deserialize(data: string): void {
    const parsed = JSON.parse(data);
    this.name = parsed.name;
    this.age = parsed.age;
  }

  log(message: string): void {
    console.log(`[User ${this.name}] ${message}`);
  }
}

Declaration Merging

Несколько объявлений interface с одним именем объединяются:

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

// Позже — в другом файле или месте
interface User {
  email: string;
  age?: number;
}

// Результат — объединение:
// interface User {
//   id: number;
//   name: string;
//   email: string;
//   age?: number;
// }

const user: User = {
  id: 1,
  name: "Alice",
  email: "alice@example.com",
};

Расширение сторонних библиотек

// Расширяем типы Express
declare module "express" {
  interface Request {
    userId?: string;
    sessionData?: Record<string, unknown>;
  }
}

// Теперь req.userId доступен без ошибок
app.use((req, res, next) => {
  req.userId = "123"; // OK
  next;
});

// Расширяем Window
interface Window {
  __APP_CONFIG__: {
    apiUrl: string;
    debug: boolean;
  };
}

window.__APP_CONFIG__.apiUrl; // OK

Index Signatures

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

// Строковый индекс
interface Dictionary {
  [key: string]: string;
}

const dict: Dictionary = {
  hello: "привет",
  world: "мир",
};

// Числовой индекс
interface StringArray {
  [index: number]: string;
}

const arr: StringArray = ["a", "b", "c"];

// Комбинация фиксированных и динамических свойств
interface Config {
  name: string; // Фиксированное свойство
  version: number; // Фиксированное свойство
  [key: string]: string | number; // Все остальные ключи
  // Фиксированные свойства должны быть совместимы с index signature
}

// Record-подобный паттерн
interface UserMap {
  [userId: string]: {
    name: string;
    age: number;
  };
}

Callable и Constructable Interfaces

// Callable — описание функции
interface Formatter {
  (input: string): string;
}

const upper: Formatter = (s) => s.toUpperCase();

// Callable с свойствами
interface Counter {
  : number;           // Вызов
  reset: void;         // Метод
  count: number;         // Свойство
}

function createCounter: Counter {
  const fn = function  {
    return ++fn.count;
  } as Counter;
  fn.count = 0;
  fn.reset() = () => { fn.count = 0; };
  return fn;
}

// Constructable — описание конструктора
interface UserConstructor {
  new (name: string, age: number): User;
}

Generics в интерфейсах

interface Repository<T> {
  getById(id: number): T | null;
  getAll: T;
  create(item: Omit<T, "id">): T;
  update(id: number, item: Partial<T>): T;
  delete(id: number): boolean;
}

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

class UserRepository implements Repository<User> {
  private users: User = ;

  getById(id: number): User | null {
    return this.users.find((u) => u.id === id) ?? null;
  }

  getAll: User {
    return [...this.users];
  }

  create(item: Omit<User, "id">): User {
    const user = { ...item, id: Date.now() };
    this.users.push(user);
    return user;
  }

  update(id: number, item: Partial<User>): User {
    const index = this.users.findIndex((u) => u.id === id);
    this.users[index] = { ...this.users[index], ...item };
    return this.users[index];
  }

  delete(id: number): boolean {
    const index = this.users.findIndex((u) => u.id === id);
    if (index === -1) return false;
    this.users.splice(index, 1);
    return true;
  }
}

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

  1. Excess property checking — лишние свойства при прямом присвоении
interface User {
  name: string;
}

// Ошибка — лишнее свойство при прямом присвоении
const user: User = { name: "Alice", age: 30 };
// Error: 'age' does not exist in type 'User'

// НО через промежуточную переменную — OK
const data = { name: "Alice", age: 30 };
const user: User = data; // OK! Структурная типизация
  1. Конфликт при declaration merging — несовместимые типы одного свойства
  2. Index signature vs конкретные свойства — конкретные свойства должны быть подтипом index signature
  3. implements не добавляет типы — класс должен реализовать всё сам
interface HasName {
  name: string;
}

class User implements HasName {
  // name НЕ добавляется автоматически — нужно объявить
  name: string; // Обязательно!
  constructor(name: string) {
    this.name = name;
  }
}
  1. Мутация readonly свойств через алиас — readonly только на уровне TypeScript

Практика

  1. Создайте интерфейс ApiResponse<T> с полями data, status, message
  2. Расширите интерфейс через extends: BaseEntityUserAdminUser
  3. Добавьте declaration merging для расширения Window
  4. Реализуйте интерфейс Repository<T> в классе
  5. Создайте callable interface с дополнительными свойствами

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

  • Type alias — альтернативный способ описания типов
  • Type vs Interface — когда что использовать
  • Классы — реализация интерфейсов
  • Generics — обобщённые интерфейсы

Ресурсы