Type vs Interface

Сравнение type и interface — два способа описания типов в TypeScript с разными возможностями и рекомендациями.

Зачем нужно

  • Понять когда использовать type, а когда interface
  • Избежать непоследовательного использования в проекте
  • Знать ограничения каждого подхода
  • Следовать best practices сообщества

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

  • В каждом TypeScript-проекте — выбор между type и interface возникает постоянно
  • Стайлгайды проектов фиксируют предпочтительный подход
  • Разные фреймворки имеют разные соглашения

Предпосылки

Общие возможности

Оба могут описывать структуру объекта:

// Interface
interface UserI {
  id: number;
  name: string;
  greet: string;
}

// Type
type UserT = {
  id: number;
  name: string;
  greet: string;
};

// Использование — идентичное
const user1: UserI = { id: 1, name: "Alice", greet:  => "Hi" };
const user2: UserT = { id: 1, name: "Alice", greet:  => "Hi" };

// Оба совместимы между собой (структурная типизация)
const user3: UserI = user2; // OK
const user4: UserT = user1; // OK

Оба поддерживают generics:

interface BoxI<T> {
  value: T;
}

type BoxT<T> = {
  value: T;
};

Оба поддерживают расширение:

// Interface extends interface
interface Animal {
  name: string;
}
interface Dog extends Animal {
  breed: string;
}

// Type extends type (через intersection)
type AnimalT = { name: string };
type DogT = AnimalT & { breed: string };

// Кросс-расширение тоже работает
interface Cat extends AnimalT {  // interface extends type
  indoor: boolean;
}

type Bird = Animal & {           // type extends interface (через &)
  canFly: boolean;
};

Уникальные возможности Interface

Declaration Merging

// Только interface — множественные объявления объединяются
interface Config {
  apiUrl: string;
}

interface Config {
  timeout: number;
}

// Результат: { apiUrl: string; timeout: number }
const config: Config = {
  apiUrl: "https://api.example.com",
  timeout: 5000,
};

// Type НЕ поддерживает merging:
type Settings = { theme: string };
type Settings = { lang: string };
// Ошибка! Duplicate identifier 'Settings'

implements в классах (формально оба, но interface предпочтительнее)

interface Serializable {
  serialize: string;
}

class User implements Serializable {
  constructor(public name: string) {}
  serialize: string {
    return JSON.stringify({ name: this.name });
  }
}

// Type тоже работает с implements, но с ограничениями:
type Loggable = {
  log: void;
};

class Logger implements Loggable {
  log: void { console.log("logged"); }
}

// НО union type нельзя implements:
type Mixed = { a: string } | { b: number };
// class Broken implements Mixed {} // Ошибка!

Уникальные возможности Type

Примитивы, union, tuple

// Type может быть примитивом
type ID = number;
type Name = string;

// Interface — нет
// interface ID = number; // Синтаксическая ошибка

// Union типы — ТОЛЬКО type
type Result = "success" | "error" | "pending";
type StringOrNumber = string | number;

// Кортежи — ТОЛЬКО type
type Point = [number, number];
type Entry = [string, unknown];

// Mapped types — ТОЛЬКО type
type Readonly<T> = { readonly [K in keyof T]: T[K] };

// Conditional types — ТОЛЬКО type
type IsString<T> = T extends string ? true : false;

// Template literal types — ТОЛЬКО type
type EventName = `on${Capitalize<string>}`;

Computed properties

type Keys = "name" | "age" | "email";

// Mapped type — только type
type UserFromKeys = {
  [K in Keys]: string;
};
// { name: string; age: string; email: string }

// Нельзя в interface:
// interface UserFromKeys {
//   [K in Keys]: string; // Синтаксическая ошибка
// }

Сравнительная таблица

Возможность interface type
Описание объекта Да Да
Generics Да Да
Extends / наследование extends & (intersection)
Implements в классах Да Да (с ограничениями)
Declaration merging Да Нет
Примитивные типы Нет Да
Union types Нет Да
Tuple types Нет Да
Mapped types Нет Да
Conditional types Нет Да
Template literal types Нет Да
Производительность компилятора Чуть лучше* Чуть хуже*

*Разница в производительности минимальна для обычных проектов.

Когда использовать Interface

  1. Описание публичного API библиотеки — declaration merging позволяет расширять
// Библиотека экспортирует interface — пользователь может расширить
export interface PluginOptions {
  name: string;
}

// Пользователь библиотеки:
declare module "my-library" {
  interface PluginOptions {
    customField: string; // Расширение через merging
  }
}
  1. Контракты для классов — implements читается лучше
interface Repository<T> {
  findById(id: number): T | null;
  save(entity: T): void;
}

class UserRepo implements Repository<User> { /* ... */ }
  1. Иерархия типов через extends
interface Shape {
  area: number;
}

interface Circle extends Shape {
  radius: number;
}

interface Rectangle extends Shape {
  width: number;
  height: number;
}

Когда использовать Type

  1. Union типы — interface не может
type Result<T> = { ok: true; value: T } | { ok: false; error: Error };
type Theme = "light" | "dark" | "system";
  1. Утилитарные и трансформированные типы
type Nullable<T> = T | null;
type ReadonlyUser = Readonly<User>;
type UserKeys = keyof User;
type PickedUser = Pick<User, "id" | "name">;
  1. Кортежи и сложные типы
type Coordinate = [number, number];
type Callback<T> = (error: Error | null, data: T) => void;
  1. Условные и mapped types
type IsArray<T> = T extends unknown ? true : false;
type Optional<T> = { [K in keyof T]?: T[K] };

Практические рекомендации

Подход 1: Interface по умолчанию (рекомендация TypeScript team)

// Для объектов — interface
interface User {
  id: number;
  name: string;
}

// Для всего остального — type
type ID = number;
type Result = "success" | "error";
type Callback = () => void;

Подход 2: Type по умолчанию (рекомендация Total TypeScript / Matt Pocock)

// Для всего — type
type User = {
  id: number;
  name: string;
};

type ID = number;
type Result = "success" | "error";

// Interface только когда нужен declaration merging
interface Window {
  __myApp: AppConfig;
}

Подход 3: Консистентность в проекте (главное правило)

// Выберите один подход и следуйте ему
// Зафиксируйте в ESLint:
// "@typescript-eslint/consistent-type-definitions": ["error", "interface"]
// или
// "@typescript-eslint/consistent-type-definitions": ["error", "type"]

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

  1. Непоследовательное использование — в одном файле interface, в другом type для одинаковых задач
  2. Declaration merging случайно — два interface с одним именем объединяются неожиданно
// Файл a.ts
interface Config { port: number; }

// Файл b.ts (случайно то же имя)
interface Config { host: string; }

// Config = { port: number; host: string } — неожиданно!
  1. interface для union — невозможно, нужен type
  2. Extends vs Intersection — разное поведение при конфликтах
interface A { x: number; }
interface B extends A { x: string; } // Ошибка! Несовместимый тип

type C = { x: number };
type D = C & { x: string }; // OK (но x: never — пересечение number & string)
  1. Слишком глубокие дебаты о type vs interface — на практике разница минимальна, важнее консистентность

Практика

  1. Перепишите набор type aliases в interface и наоборот — найдите где не получается
  2. Используйте declaration merging для расширения Window
  3. Создайте mapped type (невозможно с interface)
  4. Настройте ESLint правило consistent-type-definitions
  5. Реализуйте один и тот же контракт через interface + extends и type + &

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

Ресурсы