Type vs Interface
Сравнение
typeиinterface— два способа описания типов в TypeScript с разными возможностями и рекомендациями.
Зачем нужно
- Понять когда использовать
type, а когдаinterface - Избежать непоследовательного использования в проекте
- Знать ограничения каждого подхода
- Следовать best practices сообщества
Где используется
- В каждом TypeScript-проекте — выбор между
typeиinterfaceвозникает постоянно - Стайлгайды проектов фиксируют предпочтительный подход
- Разные фреймворки имеют разные соглашения
Предпосылки
- Type alias — type aliases
- Интерфейсы — interfaces
Общие возможности
Оба могут описывать структуру объекта:
// 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
- Описание публичного API библиотеки — declaration merging позволяет расширять
// Библиотека экспортирует interface — пользователь может расширить
export interface PluginOptions {
name: string;
}
// Пользователь библиотеки:
declare module "my-library" {
interface PluginOptions {
customField: string; // Расширение через merging
}
}
- Контракты для классов — implements читается лучше
interface Repository<T> {
findById(id: number): T | null;
save(entity: T): void;
}
class UserRepo implements Repository<User> { /* ... */ }
- Иерархия типов через extends
interface Shape {
area: number;
}
interface Circle extends Shape {
radius: number;
}
interface Rectangle extends Shape {
width: number;
height: number;
}
Когда использовать Type
- Union типы — interface не может
type Result<T> = { ok: true; value: T } | { ok: false; error: Error };
type Theme = "light" | "dark" | "system";
- Утилитарные и трансформированные типы
type Nullable<T> = T | null;
type ReadonlyUser = Readonly<User>;
type UserKeys = keyof User;
type PickedUser = Pick<User, "id" | "name">;
- Кортежи и сложные типы
type Coordinate = [number, number];
type Callback<T> = (error: Error | null, data: T) => void;
- Условные и 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"]
Частые ошибки
- Непоследовательное использование — в одном файле interface, в другом type для одинаковых задач
- Declaration merging случайно — два interface с одним именем объединяются неожиданно
// Файл a.ts
interface Config { port: number; }
// Файл b.ts (случайно то же имя)
interface Config { host: string; }
// Config = { port: number; host: string } — неожиданно!
- interface для union — невозможно, нужен type
- 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)
- Слишком глубокие дебаты о type vs interface — на практике разница минимальна, важнее консистентность
Практика
- Перепишите набор
typealiases вinterfaceи наоборот — найдите где не получается - Используйте declaration merging для расширения
Window - Создайте mapped type (невозможно с interface)
- Настройте ESLint правило
consistent-type-definitions - Реализуйте один и тот же контракт через
interface + extendsиtype + &
Связанные темы
- Type alias — type aliases подробно
- Интерфейсы — interfaces подробно
- Generics — generics в type и interface
- Mapped types — только через type