Спор type против interface обычно выглядит больше, чем он есть на самом деле. В повседневном TypeScript они действительно пересекаются: оба инструмента описывают форму данных, оба участвуют в структурной типизации, и оба подходят для типизации объектов, параметров функций и пропсов React-компонентов.
Но полное равенство здесь мнимое. Различия все же есть, и они важны не на уровне вкуса, а на уровне выразительности языка: type умеет объединения, пересечения и алиасы примитивов, а interface поддерживает declaration merging и лучше читается там, где ты описываешь расширяемый объектный контракт.
Ниже разберем, где эти различия реально влияют на код, а где спор можно просто закрыть стилевым правилом команды.
- Что у них общего
- Где без
typeне обойтись - Когда
interfaceудобнее - Расширение и совместимость
- Что выбирать для React props
- Итог
Что у них общего
Для объектных форм type и interface часто взаимозаменяемы:
interface UserFromInterface {
id: string;
name: string;
}
type UserFromType = {
id: string;
name: string;
};
const readUser = (user: UserFromInterface) => user.name;
const user: UserFromType = {
id: "1",
name: "Alex",
};
console.log(readUser(user)); // "Alex"Это работает потому, что TypeScript использует структурную типизацию. Важно не имя типа, а совпадение структуры.
Где без type не обойтись
type нужен там, где ты описываешь не только объектную форму.
Алиасы примитивов и union-типов
type Status = "idle" | "loading" | "success" | "error";
type UserId = string;
type NullableString = string | null;interface так не умеет — он не представляет объединение или алиас примитива.
Пересечения
type Timestamped = {
createdAt: string;
};
type User = {
id: string;
name: string;
};
type UserRecord = User & Timestamped;Через interface похожего эффекта тоже можно добиться, но type здесь обычно короче и понятнее.
Условные и mapped types
Почти все продвинутые утилиты TypeScript строятся через type:
type ApiResponse<T> = {
data: T;
error: string | null;
};
type ReadonlyUser<T> = {
readonly [Key in keyof T]: T[Key];
};Если в типе есть логика, почти всегда речь идет именно о type.
Когда interface удобнее
interface особенно хороша там, где ты описываешь публичный объектный контракт, который потенциально будет расширяться.
interface Animal {
age: number;
eat(): void;
}
interface Dog extends Animal {
bark(): void;
}
class Shepherd implements Dog {
age = 3;
eat() {
console.log("nom nom");
}
bark() {
console.log("woof");
}
}Такой код читается как договоренность о форме объекта. Именно поэтому многие команды по умолчанию используют interface для доменных сущностей и публичных API.
Declaration merging
Еще одно реальное отличие - interface можно дополнять повторным объявлением:
interface RequestMeta {
requestId: string;
}
interface RequestMeta {
traceId: string;
}
const meta: RequestMeta = {
requestId: "req-1",
traceId: "trace-1",
};С type так нельзя:
type RequestMeta = {
requestId: string;
};
// Error: Duplicate identifier 'RequestMeta'
type RequestMeta = {
traceId: string;
};Именно поэтому экосистема TypeScript часто использует interface для расширения внешних деклараций и библиотечных типов.
Расширение и совместимость
И interface, и type могут участвовать в композиции, если речь идет об объектных типах.
type WithId = {
id: string;
};
interface WithName {
name: string;
}
interface NamedEntity extends WithId, WithName {
kind: "user" | "team";
}
type NamedEntityAlias = WithId & WithName & {
kind: "user" | "team";
};Но здесь есть важное ограничение: interface не может расширять union-тип.
type Pet = Dog | Cat;
interface Dog {
bark(): void;
}
interface Cat {
meow(): void;
}
// Error: An interface can only extend an object type
interface HousePet extends Pet {}По той же причине класс не может implements union-тип:
type Result = SuccessResult | ErrorResult;
interface SuccessResult {
ok: true;
}
interface ErrorResult {
ok: false;
}
// Error: A class can only implement an object type or intersection of object types
class ApiResult implements Result {}Что выбирать для React props
Для props и state универсального победителя нет. Оба варианта рабочие:
type ButtonProps = {
variant: "primary" | "secondary";
onClick(): void;
};
function Button({ variant, onClick }: ButtonProps) {
return <button data-variant={variant} onClick={onClick} />;
}interface CardProps {
title: string;
children: React.ReactNode;
}
function Card({ title, children }: CardProps) {
return (
<section>
<h2>{title}</h2>
{children}
</section>
);
}Практическое правило простое:
- если тип строится из union, intersection, utility types или conditional types, используй
type; - если ты описываешь расширяемый объектный контракт, особенно публичный,
interfaceчитается лучше; - если в проекте уже есть правило команды, следуй ему, а не устраивай локальный стиль-микс без причины.
Итог
type и interface не конкурируют за одну и ту же нишу на сто процентов. type сильнее там, где нужна выразительность языка типов, а interface удобнее там, где важен расширяемый объектный контракт.
Если нужен короткий рабочий ориентир: для union-типов, utility types и сложной композиции бери type; для публичных объектных API и расширяемых деклараций бери interface. Во всех остальных случаях важнее последовательность внутри проекта, чем победа одного ключевого слова над другим.