Gernar
Frontend DeveloperTypeScript

Интервью-вопрос

Что будет, если создать два интерфейса с одинаковым именем в одном файле

TypeScript объединит такие интерфейсы в один контракт, если они находятся в одной области видимости. Главное не сказать, что второй интерфейс перезапишет первый, и не забыть про ошибки при конфликтующих членах.

Добавлен
Редакция

Подготовьте короткий ответ и пару деталей на случай уточняющих вопросов.

🐰0
🥚0

Мини-квиз

Проверка перед разбором

Несколько быстрых вопросов перед разбором. Так проще поймать места, которые только кажутся понятными.

Вопрос 1 из 60 правильно

Что ответить, если вы видите два интерфейса с одним именем?

Вы видите два объявления <code>interface User</code> в одном файле и одной области видимости.

Варианты ответа

Разбор

Разобраться, а не зазубрить

Дальше разбираем суть, типичные уточнения и места, где легко сказать лишнее или перепутать термины.

Базовая идея

В TypeScript интерфейс можно объявить несколько раз в одной области видимости. Компилятор не считает это переопределением. Он складывает объявления в один интерфейс.

interface User {
  name: string;
}

interface User {
  age: number;
}

const user: User = {
  name: "Alice",
  age: 25,
};

Практический вывод для интервью простой: объект типа User должен соответствовать полному объединенному контракту. Если вы скажете, что второй интерфейс заменит первый, пример выше сразу покажет ошибку в понимании.

Что происходит при конфликте

Declaration merging не означает, что TypeScript сам придумает безопасный компромисс. Если два объявления описывают одно и то же свойство несовместимо, это ошибка типов.

Плохой пример:

interface User {
  id: string;
}

interface User {
  id: number;
}

TypeScript не сделает id типом string | number и не выберет последнее объявление. Вам нужно явно решить, каким должен быть контракт. Например, оставить один тип id, переименовать поле или создать отдельную модель для другого слоя данных.

Какие случаи важно проговорить

Новые поляsafe

Свойства складываются. Для props или UI state это значит, что все места создания объекта должны передать новый обязательный member. Иначе сборка упадет.

То же поле и тот же типusually ok

Повторное объявление может пройти, но в коде приложения оно часто ухудшает читаемость. В React props это выглядит как случайный дубль контракта.

То же поле, другой типcompile error

TypeScript не выбирает один из типов. Исправьте контракт явно, иначе сборка остановится до того, как баг попадет в UI.

Required и optionalconflict

Модификаторы должны быть согласованы. Не рассчитывайте, что обязательное поле автоматически победит опциональное, особенно в типах данных с сервера.

Методыoverloads

Сигнатуры могут стать overloads. Проверьте порядок и специфичность, чтобы обработчики событий или API-клиенты не получили слишком широкий тип вызова.

Где это может навредить во frontend

Само merging не создает runtime-код, но оно меняет то, что TypeScript считает допустимым контрактом. Поэтому риск во frontend часто появляется на границе с реальными данными. Это ответ API, localStorage, query params, данные из CMS или props, которые собираются в разных местах приложения.

Плохой пример:

interface ApiUser {
  id: string;
  name: string;
}

interface ApiUser {
  avatarUrl: string;
}

После такого расширения компонент может поверить, что avatarUrl всегда есть. Но сервер не начнет присылать поле из-за TypeScript. Последствие: сломанная картинка, пустой alt-текст, неверный loading или empty state, иногда неверная аналитика по отображению профиля.

Безопаснее держать DTO ответа рядом с API-клиентом, валидировать внешние данные на границе и явно превращать их в модель UI. Если поле реально опционально, отразите это в типе и добавьте fallback в компоненте.

Как выбрать подход в реальном коде

В ответе хорошо отделить знание механики от стиля кода. Declaration merging полезен, когда вы расширяете уже существующий тип. Особенно тип из библиотеки или глобальный интерфейс. Например, добавляете свое поле в тип сессии, роутера или глобального объекта через поддерживаемый механизм расширения.

Но если вы просто моделируете данные приложения, два одноименных интерфейса могут запутать команду. Читателю придется искать все объявления, чтобы понять полный набор полей. В таких случаях прозрачнее написать extends, пересобрать тип явно или выбрать более точное имя.

Как объяснить выбор на интервью

1Нужно расширить тип из библиотеки или глобальный интерфейс?
Используйте declaration merging или module augmentation и держите файл расширения на виду.
2Вы описываете props, UI state или DTO внутри приложения?
Чаще выберите <code>extends</code> или новое имя. Так связь будет видна при чтении кода, а компонент не сломается из-за скрытого обязательного поля.
3Нужно сложить два type aliases?
Используйте intersection <code>A & B</code>, потому что type aliases не мержатся по имени.
4Есть одноименные поля с разной обязательностью или типом?
Не пытайтесь заставить merging решить конфликт. Сначала согласуйте один точный контракт.

Область видимости важнее имени файла

Вопрос часто звучит как "в одном файле". Там правило проще: если объявления в одной области видимости, интерфейсы объединятся. Но сильный ответ может добавить, что в больших frontend-проектах важна именно область объявления.

Если файл является ES-модулем, то есть содержит import или export, его top-level объявления живут в области этого модуля. Такой interface User не обязан объединяться с таким же именем в другом модуле. Для расширения глобальных типов или типов библиотек используют явные конструкции, например declare global или module augmentation.

Это важно на практике. Случайный глобальный интерфейс может расшириться из другого файла и сломать проверки в неожиданном месте. А надежда на merging между модулями может не сработать. Тогда код выглядит расширенным только в голове автора.

Чем это отличается от type, class и extends

Не переносите правило интерфейсов на все конструкции TypeScript. Два type с одинаковым именем в одной области видимости не объединятся. Два класса с одинаковым именем тоже не станут одним классом.

type User = {
  name: string;
};

// Плохой пример: так нельзя повторно объявить type alias с тем же именем.
type User = {
  age: number;
};

Если нужен явный составной тип, можно написать новый alias:

type NamedUser = {
  name: string;
};

type UserWithAge = NamedUser & {
  age: number;
};

Если нужен читаемый интерфейс-наследник, используйте extends. Это не то же самое, что merging, но для доменных моделей часто понятнее.

Как коротко ответить на интервью

Можно ответить так:

В TypeScript два интерфейса с одинаковым именем в одной области видимости не перезаписывают друг друга, а объединяются. Итоговый интерфейс содержит поля и методы из обоих объявлений. Если одноименные свойства конфликтуют по типу или модификаторам, будет ошибка компиляции. Я бы использовал это осознанно, например для расширения внешних типов. В обычной модели чаще сделал бы extends или новое имя.

Эта формулировка закрывает базовое правило, показывает ограничение и дает практический критерий выбора. Ее легко сократить, если интервьюер просит только поведение компилятора.

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

Где обычно ошибаются

Проверьте формулировки, которые звучат уверенно, но на интервью быстро выдают пробелы.

  1. 1

    Говорить про перезапись

    Второе объявление интерфейса не заменяет первое. Если вы так ответите, следующий пример с двумя полями сразу покажет пробел в понимании TypeScript. Правильнее сказать так: объявления объединяются в один контракт.
  2. 2

    Не учитывать область видимости

    Одинаковые имена не мержатся магически по всему проекту. В ES-модулях top-level объявления из разных файлов обычно изолированы. Глобальное расширение требует отдельного подхода, например declare global или module augmentation.
  3. 3

    Путать interface и type

    interface поддерживает declaration merging, а type с тем же именем повторно объявить нельзя. Если на интервью смешать эти механики, ответ будет звучать как заученное правило без понимания.
  4. 4

    Молчать про конфликты

    Разные типы одного поля, разные модификаторы или несовместимые объявления не склеятся безопасно. Последствие простое: сборка падает, а вы ищете случайное расширение интерфейса.
  5. 5

    Использовать merging как обычный стиль

    В прикладном коде два интерфейса с одним именем часто выглядят как дубликат и мешают понять источник полей. Если нет задачи расширить внешний или глобальный тип, обычно проще выбрать extends, композицию или новое имя.
  6. 6

    Расширять тип ответа API и забывать про реальные данные

    TypeScript не проверяет, что сервер действительно прислал новое поле. Если вы скрыто расширили интерфейс ответа и начали рисовать UI без проверки empty state, компонент может показать undefined, сломать картинку или отправить неверную аналитику. Безопаснее валидировать данные на границе и явно описывать DTO.

Follow-up

Что могут спросить дальше

Короткие ответы на вопросы, которыми интервьюер проверяет понимание declaration merging и границ этого механизма.

Живые ответы

Видео с похожим вопросом

Если найдем публичные интервью с таким вопросом, добавим их сюда. Их удобно смотреть после теории, чтобы свериться с живыми ответами.

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

Содержание