Спор type против interface обычно выглядит больше, чем он есть на самом деле. В повседневном TypeScript они действительно пересекаются: оба инструмента описывают форму данных, оба участвуют в структурной типизации, и оба подходят для типизации объектов, параметров функций и пропсов React-компонентов.

Но полное равенство здесь мнимое. Различия все же есть, и они важны не на уровне вкуса, а на уровне выразительности языка: type умеет объединения, пересечения и алиасы примитивов, а interface поддерживает declaration merging и лучше читается там, где ты описываешь расширяемый объектный контракт.

Ниже разберем, где эти различия реально влияют на код, а где спор можно просто закрыть стилевым правилом команды.

Что у них общего

Для объектных форм 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. Во всех остальных случаях важнее последовательность внутри проекта, чем победа одного ключевого слова над другим.