Skip to content

Latest commit

 

History

History
2852 lines (1897 loc) · 125 KB

File metadata and controls

2852 lines (1897 loc) · 125 KB
layout ../../layouts/Layout.astro
title TypeScript
description TypeScript, frontend fundamentals, принципы проектирования, алгоритмы, типизация и runtime-контракты
category Frontend
kind questions
order 80

TypeScript

Frontend fundamentals

В чем отличие фреймворка от библиотеки (приведите примеры и отличия)?

Короткий ответ

Библиотека решает отдельную задачу, а приложение само определяет архитектуру и момент вызова библиотеки. Примеры: RxJS, Lodash, date-fns.

Полный ответ

Библиотека решает отдельную задачу, а приложение само определяет архитектуру и момент вызова библиотеки. Примеры: RxJS, Lodash, date-fns.

Фреймворк задает каркас приложения, жизненный цикл, правила организации кода и сам вызывает пользовательский код в нужный момент. Это называют inversion of control. Примеры: Angular, NestJS.

Angular предоставляет не только рендеринг, но и DI, Router, формы, HTTP-клиент, компиляцию шаблонов, CLI и инструменты тестирования. Поэтому Angular является платформой и фреймворком, а не просто UI-библиотекой.

Какие популярные CSS, JS библиотеки вы знаете?

Короткий ответ

Примеры, которые уместно назвать вместе с их назначением:

Полный ответ

Примеры, которые уместно назвать вместе с их назначением:

  • UI и CSS: Angular Material, Taiga UI, Bootstrap, Tailwind CSS.
  • Реактивность: RxJS.
  • Работа с датами: date-fns, Luxon.
  • Утилиты: Lodash.
  • Графики: D3.js, Chart.js, ECharts.
  • Тестирование: Vitest, Jest, Jasmine, Cypress, Playwright.
  • Управление состоянием Angular: NgRx, NGXS.

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

Принципы проектирования

Что такое SOLID?

Короткий ответ

SOLID - пять принципов проектирования:

Полный ответ

img.png

SOLID - пять принципов проектирования:

  • SRP: у модуля должна быть одна основная причина для изменения.
  • OCP: поведение лучше расширять через новые реализации, а не растущий if.
  • LSP: реализация должна соблюдать контракт базового типа.
  • ISP: лучше несколько узких интерфейсов, чем один универсальный.
  • DIP: бизнес-логика должна зависеть от абстракций, а не от HTTP, storage или других деталей.

На практике SOLID помогает уменьшить связанность и упростить тестирование. Это ориентиры, а не требование создавать отдельный класс для каждой функции.

Что такое DRY?

Короткий ответ

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

Полный ответ

img.png

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

Одинаковые строки кода не всегда являются дублированием: два похожих сценария можно оставить раздельными, если они меняются по разным причинам.

Что такое KISS?

Короткий ответ

KISS предлагает выбирать самое простое решение, которое корректно выполняет текущие требования.

Полный ответ

img.png

KISS предлагает выбирать самое простое решение, которое корректно выполняет текущие требования.

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

Что такое YAGNI?

Короткий ответ

YAGNI означает: не реализовывать функциональность, пока для нее нет подтвержденной потребности.

Полный ответ

img.png

YAGNI означает: не реализовывать функциональность, пока для нее нет подтвержденной потребности.

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

Что такое cohesion и coupling?

Короткий ответ

Cohesion показывает, насколько логика внутри модуля относится к одной задаче. Coupling показывает, насколько сильно модуль зависит от деталей других модулей.

Полный ответ

Cohesion показывает, насколько логика внутри модуля относится к одной задаче. Coupling показывает, насколько сильно модуль зависит от деталей других модулей.

Обычно стремятся к высокой cohesion и низкому явному coupling: например, состояние корзины и расчет суммы можно хранить вместе, а аналитику и HTTP вынести за ее границы.

Что такое code smell и technical debt?

Короткий ответ

Code smell - признак возможной проблемы дизайна: большой компонент, длинный список зависимостей, boolean-флаги с противоречивыми состояниями или повторение бизнес-правил.

Полный ответ

Code smell - признак возможной проблемы дизайна: большой компонент, длинный список зависимостей, boolean-флаги с противоречивыми состояниями или повторение бизнес-правил.

Technical debt - будущая стоимость сделанного упрощения. Он допустим как осознанный компромисс с ограниченным риском, тестовой страховкой и планом пересмотра.

Что такое рефакторинг?

Короткий ответ

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

Полный ответ

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

Если небольшое бизнес-правило нельзя проверить без большого TestBed, его стоит отделить от I/O и Angular APIs, например вынести в чистую функцию или узкий сервис.

Как могут конфликтовать SOLID, DRY, KISS и YAGNI?

Короткий ответ

Принципы могут подталкивать к разным решениям:

Полный ответ

Принципы могут подталкивать к разным решениям:

  • DRY предлагает вынести повторение, а KISS может оставить два простых независимых фрагмента.
  • OCP предлагает точку расширения, а YAGNI не позволяет проектировать ее без реального сценария.
  • SRP помогает разделить обязанности, но чрезмерное дробление ухудшает навигацию.

Приоритет отдают текущим требованиям и стоимости изменений. Сначала пишут ясное рабочее решение, а абстракцию добавляют после появления устойчивого повторения или вариативности.

Как применять инженерные принципы в Angular?

Короткий ответ

UserFacade зависит от узкого контракта, а HTTP-реализацию можно заменить provider-ом или тестовым fake.

Полный ответ

  • Компонент отвечает за UI и события пользователя.
  • Сервис или facade координирует сценарий.
  • Чистая функция содержит вычисления и преобразования.
  • DI и InjectionToken позволяют заменить инфраструктурную реализацию.
  • Signals подходят для локального синхронного состояния, RxJS - для сложных асинхронных потоков.
export interface UserRepository {
  findById(id: string): Observable<User>;
}

export const USER_REPOSITORY = new InjectionToken<UserRepository>('USER_REPOSITORY');

@Injectable({providedIn: 'root'})
export class UserFacade {
  private readonly repository = inject(USER_REPOSITORY);

  load(id: string): Observable<User> {
    return this.repository.findById(id);
  }
}

UserFacade зависит от узкого контракта, а HTTP-реализацию можно заменить provider-ом или тестовым fake.

Что лучше: композиция или наследование?

Короткий ответ

Во frontend чаще выбирают композицию: компоненты, сервисы, директивы, content projection, host directives и DI можно сочетать без глубокой иерархии классов.

Полный ответ

Во frontend чаще выбирают композицию: компоненты, сервисы, директивы, content projection, host directives и DI можно сочетать без глубокой иерархии классов.

Наследование уместно, когда существует устойчивое отношение «является» и подкласс полностью соблюдает контракт базового типа.

Парадигмы и базовые CS-темы

Что такое функциональное программирование?

Короткий ответ

Функциональное программирование строит вычисления вокруг функций и преобразований данных. Практические идеи:

Полный ответ

Функциональное программирование строит вычисления вокруг функций и преобразований данных. Практические идеи:

  • pure functions без скрытых side effects;
  • immutable data;
  • композиция небольших функций;
  • декларативные операции map, filter, reduce;
  • явное отделение вычислений от I/O.

Во frontend это упрощает тестирование и предсказуемость состояния. Полностью избегать мутаций не обязательно: важно локализовать их на понятных границах.

Назовите основные принципы ООП?

Короткий ответ

ООП не требует применять наследование везде. В Angular чаще полезны композиция компонентов и сервисов, DI и небольшие интерфейсы.

Полный ответ

  • Инкапсуляция — объект скрывает внутреннее состояние и предоставляет контролируемый API.
  • Абстракция — наружу выносится существенное поведение, детали реализации скрываются.
  • Наследование — новый тип переиспользует и расширяет поведение базового типа.
  • Полиморфизм — разные реализации используются через общий контракт.

ООП не требует применять наследование везде. В Angular чаще полезны композиция компонентов и сервисов, DI и небольшие интерфейсы.

Пример сочетает четыре принципа:

interface NotificationChannel {
  send(message: string): void;
}

abstract class BaseNotificationChannel implements NotificationChannel {
  constructor(private readonly prefix: string) {}

  protected format(message: string): string {
    return `${this.prefix}: ${message}`;
  }

  abstract send(message: string): void;
}

class EmailChannel extends BaseNotificationChannel {
  send(message: string): void {
    sendEmail(this.format(message));
  }
}

class PushChannel extends BaseNotificationChannel {
  send(message: string): void {
    sendPush(this.format(message));
  }
}

function notify(channel: NotificationChannel, message: string): void {
  channel.send(message);
}
  • private prefix демонстрирует инкапсуляцию.
  • NotificationChannel и BaseNotificationChannel задают абстракцию.
  • EmailChannel и PushChannel используют наследование.
  • notify() работает полиморфно с любой реализацией контракта.

Отдельно часто спрашивают SOLID: пять принципов проектирования, которые помогают уменьшать связанность и делать код расширяемым и тестируемым.

Диаграммы и моделирование

Sequence diagram / диаграмма последовательности

Что такое participant, lifeline, message и response?

Короткий ответ

Participant - участник сценария, например Component, Service, Interceptor или Backend. Lifeline - вертикальная линия жизни участника во времени. Message - вызов или событие между участниками. Response - ответ на вызов или результат обработки.

Полный ответ

Participant - участник сценария, например Component, Service, Interceptor или Backend. Lifeline - вертикальная линия жизни участника во времени. Message - вызов или событие между участниками. Response - ответ на вызов или результат обработки.

Как показать retry, timeout или fallback?

Короткий ответ

Retry удобно показывать через loop, timeout - через отдельный error/timeout branch, fallback - как альтернативный путь после неуспешного вызова. На диаграмме должно быть видно условие перехода и кто принимает решение о повторе или fallback.

Полный ответ

Retry удобно показывать через loop, timeout - через отдельный error/timeout branch, fallback - как альтернативный путь после неуспешного вызова. На диаграмме должно быть видно условие перехода и кто принимает решение о повторе или fallback.

sequenceDiagram
  Component->>Service: load()
  loop up to 3 attempts
    Service->>Backend: GET /items
    Backend-->>Service: timeout
  end
  Service-->>Component: fallback data
Loading
Что такое sequence diagram и для чего она нужна?

Короткий ответ

Sequence diagram показывает, как участники системы обмениваются сообщениями во времени. Она помогает разобрать порядок вызовов, границы ответственности, async flows, ошибки и интеграции между frontend, backend и внешними сервисами.

Полный ответ

Sequence diagram показывает, как участники системы обмениваются сообщениями во времени. Она помогает разобрать порядок вызовов, границы ответственности, async flows, ошибки и интеграции между frontend, backend и внешними сервисами.

Чем sequence diagram отличается от flowchart?

Короткий ответ

Sequence diagram фокусируется на взаимодействии участников и порядке сообщений между ними. Flowchart фокусируется на ветвлениях, шагах алгоритма и принятии решений внутри процесса.

Полный ответ

Sequence diagram фокусируется на взаимодействии участников и порядке сообщений между ними. Flowchart фокусируется на ветвлениях, шагах алгоритма и принятии решений внутри процесса.

Почему sequence diagram читается сверху вниз?

Короткий ответ

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

Полный ответ

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

Как на sequence diagram показать синхронный вызов, асинхронное сообщение и ответ?

Короткий ответ

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

Полный ответ

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

sequenceDiagram
  Component->>Service: load()
  Service-->>Component: data
Loading
Как показать альтернативные сценарии: success / error, cache hit / cache miss?

Короткий ответ

Альтернативы показывают через блоки alt / else. В каждом блоке оставляют только сообщения, которые относятся к этому варианту, чтобы success, error, cache hit и cache miss не смешивались в одну линию.

Полный ответ

Альтернативы показывают через блоки alt / else. В каждом блоке оставляют только сообщения, которые относятся к этому варианту, чтобы success, error, cache hit и cache miss не смешивались в одну линию.

sequenceDiagram
  Component->>Service: load()
  alt cache hit
    Service-->>Component: cached data
  else cache miss
    Service->>Backend: GET /items
    Backend-->>Service: items
  end
Loading
Когда sequence diagram полезна frontend-разработчику?

Короткий ответ

Она полезна при разборе авторизации, refresh token flow, загрузки данных, race conditions, optimistic updates, interceptors, guards, WebSocket-сценариев и взаимодействия нескольких сервисов. Диаграмма быстро показывает, где живет логика и какие ошибки нужно обработать.

Полный ответ

Она полезна при разборе авторизации, refresh token flow, загрузки данных, race conditions, optimistic updates, interceptors, guards, WebSocket-сценариев и взаимодействия нескольких сервисов. Диаграмма быстро показывает, где живет логика и какие ошибки нужно обработать.

Какие признаки плохой или перегруженной sequence diagram?

Короткий ответ

Слишком много участников, смешение нескольких независимых сценариев, отсутствие условий, неясные названия сообщений, детали реализации вместо бизнес-событий и попытка показать всю систему на одной диаграмме. Хорошая диаграмма отвечает на один конкретный вопрос.

Полный ответ

Слишком много участников, смешение нескольких независимых сценариев, отсутствие условий, неясные названия сообщений, детали реализации вместо бизнес-событий и попытка показать всю систему на одной диаграмме. Хорошая диаграмма отвечает на один конкретный вопрос.

Нарисуйте sequence diagram для сценария refresh token в Angular.

Короткий ответ

В ответе важно показать, что refresh выполняет interceptor, исходный запрос повторяется после получения нового access token, а component получает результат как обычный ответ сервиса.

Полный ответ

sequenceDiagram
  participant Component
  participant Service
  participant Interceptor
  participant Backend

  Component->>Service: loadData()
  Service->>Interceptor: HTTP request
  Interceptor->>Backend: request with access token
  Backend-->>Interceptor: 401
  Interceptor->>Backend: refresh token
  Backend-->>Interceptor: new access token
  Interceptor->>Backend: retry original request
  Backend-->>Interceptor: data
  Interceptor-->>Service: data
  Service-->>Component: data
Loading

В ответе важно показать, что refresh выполняет interceptor, исходный запрос повторяется после получения нового access token, а component получает результат как обычный ответ сервиса.

Flowchart / блок-схема

Что такое flowchart и для чего она нужна?

Короткий ответ

Flowchart, или блок-схема, показывает шаги процесса, условия и переходы между ними. Она помогает обсудить алгоритм, валидацию, бизнес-правила или decision tree без привязки к конкретному коду.

Полный ответ

Flowchart, или блок-схема, показывает шаги процесса, условия и переходы между ними. Она помогает обсудить алгоритм, валидацию, бизнес-правила или decision tree без привязки к конкретному коду.

Какие базовые элементы flowchart вы знаете: start/end, process, decision, input/output?

Короткий ответ

Start/end обозначает начало и завершение процесса. Process - действие или вычисление. Decision - условие с несколькими ветками. Input/output - получение входных данных или вывод результата.

Полный ответ

Start/end обозначает начало и завершение процесса. Process - действие или вычисление. Decision - условие с несколькими ветками. Input/output - получение входных данных или вывод результата.

Как на flowchart показать условие if/else?

Короткий ответ

Условие показывают decision-блоком с двумя или несколькими исходящими ветками. Ветки подписывают значениями условия, например yes / no, valid / invalid, success / error.

Полный ответ

Условие показывают decision-блоком с двумя или несколькими исходящими ветками. Ветки подписывают значениями условия, например yes / no, valid / invalid, success / error.

flowchart TD
  A[Start] --> B{User authorized?}
  B -->|yes| C[Show content]
  B -->|no| D[Show login]
Loading
Как на flowchart показать цикл?

Короткий ответ

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

Полный ответ

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

flowchart TD
  A[Take next item] --> B{Has item?}
  B -->|yes| C[Process item]
  C --> A
  B -->|no| D[Finish]
Loading
Как flowchart помогает разобрать сложную бизнес-логику?

Короткий ответ

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

Полный ответ

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

Какие проблемы появляются, если пытаться описать слишком большую систему одной flowchart?

Короткий ответ

Схема становится нечитаемой: слишком много веток, пересечений, уровней детализации и скрытых правил. Лучше разделять ее на несколько схем: общий happy path, ошибки, отдельные доменные процессы и редкие edge cases.

Полный ответ

Схема становится нечитаемой: слишком много веток, пересечений, уровней детализации и скрытых правил. Лучше разделять ее на несколько схем: общий happy path, ошибки, отдельные доменные процессы и редкие edge cases.

Нарисуйте flowchart для логики показа кнопки "Купить".

Короткий ответ

Хороший ответ показывает не только итог enabled, но и причину, которую нужно отобразить пользователю при каждом неуспешном условии.

Полный ответ

flowchart TD
  A[Start] --> B{User authorized?}
  B -->|no| C[Show reason: login required]
  B -->|yes| D{Product in stock?}
  D -->|no| E[Show reason: out of stock]
  D -->|yes| F{User not blocked?}
  F -->|no| G[Show reason: user blocked]
  F -->|yes| H{Delivery available?}
  H -->|no| I[Show reason: delivery unavailable]
  H -->|yes| J[Show enabled Buy button]
Loading

Хороший ответ показывает не только итог enabled, но и причину, которую нужно отобразить пользователю при каждом неуспешном условии.

Чем flowchart отличается от sequence diagram?

Короткий ответ

Flowchart отвечает на вопрос "какие шаги и условия есть в процессе". Sequence diagram отвечает на вопрос "какие участники обмениваются сообщениями и в каком порядке".

Полный ответ

Flowchart отвечает на вопрос "какие шаги и условия есть в процессе". Sequence diagram отвечает на вопрос "какие участники обмениваются сообщениями и в каком порядке".

Когда flowchart лучше подходит, чем sequence diagram?

Короткий ответ

Flowchart лучше подходит для условий, алгоритмов, валидации формы, расчета статуса, показа UI-состояний и бизнес-правил, где важнее decision logic, чем обмен сообщениями между несколькими участниками.

Полный ответ

Flowchart лучше подходит для условий, алгоритмов, валидации формы, расчета статуса, показа UI-состояний и бизнес-правил, где важнее decision logic, чем обмен сообщениями между несколькими участниками.

ER diagram / Entity Relationship Diagram

Что такое attribute / field?

Короткий ответ

Attribute или field - свойство сущности: id, email, status, createdAt, price. В ER diagram поля помогают увидеть, какие данные обязательны, какие являются идентификаторами и какие участвуют в связях.

Полный ответ

Attribute или field - свойство сущности: id, email, status, createdAt, price. В ER diagram поля помогают увидеть, какие данные обязательны, какие являются идентификаторами и какие участвуют в связях.

Что такое primary key?

Короткий ответ

Primary key - поле или набор полей, которые уникально идентифицируют запись. Для frontend это часто стабильный id, который используют в URL, normalized state, списках, track expressions и API-запросах.

Полный ответ

Primary key - поле или набор полей, которые уникально идентифицируют запись. Для frontend это часто стабильный id, который используют в URL, normalized state, списках, track expressions и API-запросах.

Что такое foreign key?

Короткий ответ

Foreign key - поле, которое ссылается на primary key другой сущности. Например, booking.userId связывает бронирование с пользователем, а session.movieId связывает сеанс с фильмом.

Полный ответ

Foreign key - поле, которое ссылается на primary key другой сущности. Например, booking.userId связывает бронирование с пользователем, а session.movieId связывает сеанс с фильмом.

Что такое junction table / linking table?

Короткий ответ

Junction table, или linking table, - таблица-связка для many-to-many. Например, BookingSeat может связывать Booking и Seat, если в одном бронировании может быть несколько мест, а место участвует в разных бронированиях для разных сеансов.

Полный ответ

Junction table, или linking table, - таблица-связка для many-to-many. Например, BookingSeat может связывать Booking и Seat, если в одном бронировании может быть несколько мест, а место участвует в разных бронированиях для разных сеансов.

Чем entity отличается от table?

Короткий ответ

Entity - сущность предметной области, например User или Booking. Table - конкретное хранение этой сущности в базе данных. В простой системе они могут почти совпадать, но entity описывает смысл, а table - техническую реализацию.

Полный ответ

Entity - сущность предметной области, например User или Booking. Table - конкретное хранение этой сущности в базе данных. В простой системе они могут почти совпадать, но entity описывает смысл, а table - техническую реализацию.

Что такое ER diagram и для чего она нужна?

Короткий ответ

ER diagram показывает сущности предметной области, их поля и связи. Она помогает понять структуру данных, ограничения, отношения между объектами и то, как эти данные могут приходить во frontend через API.

Полный ответ

ER diagram показывает сущности предметной области, их поля и связи. Она помогает понять структуру данных, ограничения, отношения между объектами и то, как эти данные могут приходить во frontend через API.

Какие бывают связи между сущностями: one-to-one, one-to-many, many-to-many?

Короткий ответ

One-to-one: одной записи соответствует одна другая запись. One-to-many: одна запись связана со многими, например Movie и Sessions. Many-to-many: многие записи связаны со многими, например Users и Roles или Students и Courses.

Полный ответ

One-to-one: одной записи соответствует одна другая запись. One-to-many: одна запись связана со многими, например Movie и Sessions. Many-to-many: многие записи связаны со многими, например Users и Roles или Students и Courses.

Как смоделировать many-to-many связь?

Короткий ответ

Many-to-many обычно моделируют через отдельную связующую сущность. Она хранит ссылки на обе стороны связи и может иметь собственные поля: дату создания, статус, порядок, роль или цену.

Полный ответ

Many-to-many обычно моделируют через отдельную связующую сущность. Она хранит ссылки на обе стороны связи и может иметь собственные поля: дату создания, статус, порядок, роль или цену.

Почему frontend-разработчику полезно понимать ER diagrams?

Короткий ответ

Так проще понимать API, нормализовать данные, проектировать state, ловить неоднозначности в DTO и задавать backend правильные вопросы. ER diagram помогает увидеть, какие данные являются справочниками, какие зависят от пользователя, а какие являются результатом операции.

Полный ответ

Так проще понимать API, нормализовать данные, проектировать state, ловить неоднозначности в DTO и задавать backend правильные вопросы. ER diagram помогает увидеть, какие данные являются справочниками, какие зависят от пользователя, а какие являются результатом операции.

Как ER diagram помогает понять API и структуру данных на frontend?

Короткий ответ

Она показывает, какие объекты могут приходить вложенными, где нужны ids, какие связи надо дозагружать и какие поля нельзя редактировать напрямую. Это влияет на типы DTO, формы, кеширование, optimistic updates и normalized stores.

Полный ответ

Она показывает, какие объекты могут приходить вложенными, где нужны ids, какие связи надо дозагружать и какие поля нельзя редактировать напрямую. Это влияет на типы DTO, формы, кеширование, optimistic updates и normalized stores.

Чем ER diagram отличается от class diagram?

Короткий ответ

ER diagram описывает данные и связи предметной области. Class diagram описывает классы, методы, наследование, интерфейсы и объектную модель кода. Они могут пересекаться по названиям сущностей, но отвечают на разные вопросы.

Полный ответ

ER diagram описывает данные и связи предметной области. Class diagram описывает классы, методы, наследование, интерфейсы и объектную модель кода. Они могут пересекаться по названиям сущностей, но отвечают на разные вопросы.

Нарисуйте ER diagram для простой системы бронирования билетов в кино.

Короткий ответ

На собеседовании можно обсудить, нужна ли отдельная сущность BookingSeat, если одно бронирование содержит несколько мест или если у места есть цена на момент покупки.

Полный ответ

erDiagram
  User ||--o{ Booking : creates
  Movie ||--o{ Session : has
  Session ||--o{ Seat : contains
  Session ||--o{ Booking : booked_for
  Booking ||--o{ Seat : reserves
  Booking ||--o| Payment : paid_by

  User {
    string id
    string email
  }

  Movie {
    string id
    string title
  }

  Session {
    string id
    string movieId
    datetime startsAt
  }

  Seat {
    string id
    string sessionId
    string row
    string number
  }

  Booking {
    string id
    string userId
    string sessionId
    string status
  }

  Payment {
    string id
    string bookingId
    string status
  }
Loading

На собеседовании можно обсудить, нужна ли отдельная сущность BookingSeat, если одно бронирование содержит несколько мест или если у места есть цена на момент покупки.

Инструменты

Почему Mermaid удобно использовать в GitHub README?

Короткий ответ

GitHub умеет рендерить Mermaid прямо в Markdown. Диаграмма хранится как текст рядом с документацией, ее легко читать в diff, ревьюить, менять вместе с кодом и поддерживать без отдельных бинарных файлов.

Полный ответ

GitHub умеет рендерить Mermaid прямо в Markdown. Диаграмма хранится как текст рядом с документацией, ее легко читать в diff, ревьюить, менять вместе с кодом и поддерживать без отдельных бинарных файлов.

Какие инструменты можно использовать для описания диаграмм в документации: Mermaid, PlantUML, draw.io / diagrams.net?

Короткий ответ

Mermaid и PlantUML описывают диаграммы текстом, поэтому хорошо подходят для version control и code review. draw.io / diagrams.net удобен для визуального редактирования, особенно когда нужна свободная компоновка или схема для презентации.

Полный ответ

Mermaid и PlantUML описывают диаграммы текстом, поэтому хорошо подходят для version control и code review. draw.io / diagrams.net удобен для визуального редактирования, особенно когда нужна свободная компоновка или схема для презентации.

Алгоритмы и структуры данных

Что такое структура данных и какие виды вы знаете (Стек, etc)?

Короткий ответ

Структура данных — способ организовать данные и операции над ними.

Полный ответ

Структура данных — способ организовать данные и операции над ними.

  • Массив: быстрый доступ по индексу, последовательное хранение.
  • Связный список: удобные вставки и удаления при наличии ссылки на узел.
  • Стек: LIFO, пример — call stack.
  • Очередь: FIFO, пример — очередь задач.
  • Map: пары ключ-значение с ключами любого типа.
  • Set: множество уникальных значений.
  • Дерево: иерархия, пример — DOM и дерево компонентов.
  • Граф: вершины и связи, пример — зависимости модулей.
  • Heap: структура для быстрого получения минимального или максимального элемента.

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

Чем Θ-нотация отличается от O-нотации?

Короткий ответ

O(f(n)) задает верхнюю границу роста, а Θ(f(n)) - точную асимптотическую оценку сверху и снизу.

Полный ответ

O(f(n)) задает верхнюю границу роста, а Θ(f(n)) - точную асимптотическую оценку сверху и снизу.

Например, полный проход по массиву всегда выполняет пропорциональное n число шагов: это Θ(n) и одновременно O(n). На frontend-собеседованиях чаще используют Big O, но важно понимать, что это оценка роста, а не точное время в миллисекундах.

Как оптимизировать перебор двумя циклами?

Короткий ответ

Часто один набор заранее индексируют через Map или Set, заменяя повторный линейный поиск на lookup:

Полный ответ

Часто один набор заранее индексируют через Map или Set, заменяя повторный линейный поиск на lookup:

const usersById = new Map(users.map((user) => [user.id, user]));

const ordersWithUsers = orders.map((order) => ({
  ...order,
  user: usersById.get(order.userId),
}));

Вместо примерно O(n * m) получается O(n + m) по времени ценой дополнительной памяти.

Чем линейный поиск отличается от бинарного?

Короткий ответ

Линейный поиск проверяет элементы последовательно и работает за O(n). Он подходит для небольшого или неотсортированного списка.

Полный ответ

Линейный поиск проверяет элементы последовательно и работает за O(n). Он подходит для небольшого или неотсортированного списка.

Бинарный поиск делит область поиска пополам и работает за O(log n), но требует отсортированных данных и доступа по индексу. Предварительная сортировка стоит O(n log n), поэтому она не всегда окупается для одного поиска.

Какой алгоритм сортировки полезно знать?

Короткий ответ

Merge sort делит массив пополам, сортирует части и сливает их. Временная сложность - O(n log n), дополнительная память - O(n).

Полный ответ

Merge sort делит массив пополам, сортирует части и сливает их. Временная сложность - O(n log n), дополнительная память - O(n).

В прикладном frontend-коде обычно используют встроенный toSorted(comparator). Важно уметь написать comparator и понимать, что сортировка больших таблиц может потребовать memoization, worker или переноса на сервер.

Как следить за чистотой кода?

Короткий ответ

Помогают небольшие функции, точные имена, строгие типы, явные состояния, отсутствие скрытых side effects и регулярное удаление мертвого кода.

Полный ответ

Помогают небольшие функции, точные имена, строгие типы, явные состояния, отсутствие скрытых side effects и регулярное удаление мертвого кода.

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

Что такое сложность алгоритмов?

Короткий ответ

Big O показывает верхнюю асимптотическую границу роста времени или памяти при увеличении входных данных. Константы и небольшие слагаемые обычно отбрасывают.

Полный ответ

Big O показывает верхнюю асимптотическую границу роста времени или памяти при увеличении входных данных. Константы и небольшие слагаемые обычно отбрасывают.

  • O(1) - доступ к элементу Map в среднем;
  • O(log n) - бинарный поиск в отсортированном массиве;
  • O(n) - один проход по массиву;
  • O(n log n) - типичная сортировка;
  • O(n²) - вложенное сравнение всех элементов.

Один цикл по n элементам обычно имеет O(n), два независимых последовательных цикла тоже O(n), а два вложенных цикла часто дают O(n²).

Как измерять сложность алгоритма?

Короткий ответ

Определяют размер входа n, считают наиболее часто выполняемые операции и оставляют доминирующий член.

Полный ответ

Определяют размер входа n, считают наиболее часто выполняемые операции и оставляют доминирующий член.

Помимо времени учитывают память, реальные размеры данных и стоимость операций. Профилирование дополняет асимптотическую оценку: алгоритм с лучшим Big O не всегда быстрее на маленьком входе.

Какова сложность доступа в основных структурах данных?

Короткий ответ

В JavaScript для очереди частый shift() массива имеет O(n). Для большой очереди лучше использовать индекс начала или специализированную структуру.

Полный ответ

  • Массив: доступ по индексу O(1), поиск по значению O(n), вставка в середину O(n).
  • Связный список: доступ по позиции O(n), вставка по известной ссылке O(1).
  • Map и Set: lookup, добавление и удаление в среднем O(1).
  • Стек и очередь: добавление и удаление с рабочего конца обычно O(1).

В JavaScript для очереди частый shift() массива имеет O(n). Для большой очереди лучше использовать индекс начала или специализированную структуру.

Алгоритмы и структуры данных для frontend

Что такое дерево и где деревья встречаются в Angular-приложении?

Короткий ответ

Дерево — иерархическая структура из узлов, где у узла может быть родитель и дочерние узлы. Во frontend деревья встречаются постоянно: DOM tree, accessibility tree, component tree, Angular Router tree, DI tree, Reactive Forms tree, AST в TypeScript/ESLint/Babel.

Полный ответ

Дерево — иерархическая структура из узлов, где у узла может быть родитель и дочерние узлы. Во frontend деревья встречаются постоянно: DOM tree, accessibility tree, component tree, Angular Router tree, DI tree, Reactive Forms tree, AST в TypeScript/ESLint/Babel.

Обход дерева нужен для поиска узла, валидации вложенной формы, построения меню, анализа AST или сериализации состояния. DFS удобно идет вглубь ветки, BFS обходит уровень за уровнем.

function collectInvalidControls(control: AbstractControl): AbstractControl[] {
  const invalidControls: AbstractControl[] = [];
  const stack: AbstractControl[] = [control];

  while (stack.length > 0) {
    const current = stack.pop();

    if (current === undefined) {
      continue;
    }

    if (current.invalid) {
      invalidControls.push(current);
    }

    if ('controls' in current) {
      stack.push(...Object.values(current.controls));
    }
  }

  return invalidControls;
}
Что такое граф и где графы встречаются во frontend-разработке?

Короткий ответ

Граф состоит из вершин и связей между ними. Directed graph имеет направленные ребра, undirected graph — связи без направления. Граф может храниться как adjacency list или adjacency matrix.

Полный ответ

Граф состоит из вершин и связей между ними. Directed graph имеет направленные ребра, undirected graph — связи без направления. Граф может храниться как adjacency list или adjacency matrix.

Во frontend графы встречаются в routing flows, state machines, dependency graph bundler-а, Nx affected graph, module federation dependencies, диаграммах, permission models и формах с зависимыми полями.

Cycle в dependency graph часто означает архитектурную проблему: модули начинают зависеть друг от друга, сборка и тесты становятся хрупкими. DFS помогает находить cycle, BFS — искать кратчайший путь по числу переходов в простом графе.

Что такое NP-complete и нужно ли это frontend-разработчику?

Короткий ответ

NP-complete — класс задач, для которых неизвестен быстрый алгоритм общего вида, а перебор вариантов быстро становится непрактичным. Для frontend это редко повседневная тема, но идея полезна при обсуждении scheduling, layout, оптимизации маршрутов, упаковки элементов и сложных combinatorial constraints.

Полный ответ

NP-complete — класс задач, для которых неизвестен быстрый алгоритм общего вида, а перебор вариантов быстро становится непрактичным. Для frontend это редко повседневная тема, но идея полезна при обсуждении scheduling, layout, оптимизации маршрутов, упаковки элементов и сложных combinatorial constraints.

На интервью достаточно объяснить практический вывод: если точное решение слишком дорогое, выбирают ограничения, эвристики, приближенный алгоритм, предварительный расчет на сервере или упрощение продукта.

Почему Array.includes внутри filter может случайно дать O(n²)?

Короткий ответ

includes() ищет элемент линейно. Если вызвать его для каждого элемента другого массива, получится вложенный перебор. На маленьком списке это нормально, но на тысячах строк таблицы может стать bottleneck.

Полный ответ

includes() ищет элемент линейно. Если вызвать его для каждого элемента другого массива, получится вложенный перебор. На маленьком списке это нормально, но на тысячах строк таблицы может стать bottleneck.

const selectedUsers = users.filter((user) => selectedIds.includes(user.id));

Если selectedIds большой и используется многократно, лучше построить Set один раз:

const selectedIdSet = new Set(selectedIds);
const selectedUsers = users.filter((user) => selectedIdSet.has(user.id));

Так поиск по выбранным id становится в среднем O(1), а общий проход — O(n + m) по времени ценой дополнительной памяти.

Как выбрать между Array, Object, Map и Set?

Короткий ответ

Для UI-состояния структура должна выражать сценарий. Если список нужен для отображения, часто оставляют Array. Если нужны частые проверки выбранности, рядом хранят Set id. Если нужно быстро получить сущность по id, добавляют Map.

Полный ответ

Структура Когда использовать Важный компромисс
Array Упорядоченный список, rendering через @for, последовательный обход Поиск по значению обычно O(n)
Object Простая JSON-like запись со строковыми ключами Не подходит для произвольных ключей
Map Индекс по id, кеш по объекту или составному ключу, частые lookup/delete Больше overhead, чем у plain object
Set Уникальные значения, проверка принадлежности, дедупликация Хранит только значения, без payload

Для UI-состояния структура должна выражать сценарий. Если список нужен для отображения, часто оставляют Array. Если нужны частые проверки выбранности, рядом хранят Set id. Если нужно быстро получить сущность по id, добавляют Map.

Что значит stable sort и почему это важно для UI?

Короткий ответ

Stable sort сохраняет относительный порядок элементов, которые считаются равными comparator-ом. Это важно для таблиц: если пользователь сначала отсортировал список по имени, а потом по роли, элементы с одинаковой ролью могут сохранить предыдущий порядок по имени.

Полный ответ

Stable sort сохраняет относительный порядок элементов, которые считаются равными comparator-ом. Это важно для таблиц: если пользователь сначала отсортировал список по имени, а потом по роли, элементы с одинаковой ролью могут сохранить предыдущий порядок по имени.

Современный JavaScript требует стабильную сортировку Array.prototype.sort. Но comparator все равно должен быть корректным: возвращать отрицательное число, ноль или положительное число согласованно и не зависеть от случайного состояния.

const sorted = users.toSorted((first, second) => {
  const byRole = first.role.localeCompare(second.role);

  return byRole === 0 ? first.name.localeCompare(second.name) : byRole;
});
Чем отличаются preorder, inorder, postorder, DFS и BFS?

Короткий ответ

DFS идет в глубину. Preorder обрабатывает узел до детей, postorder — после детей, inorder применим прежде всего к бинарным деревьям и обходит левую ветку, узел, правую ветку.

Полный ответ

DFS идет в глубину. Preorder обрабатывает узел до детей, postorder — после детей, inorder применим прежде всего к бинарным деревьям и обходит левую ветку, узел, правую ветку.

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

В Angular-практике preorder похож на обработку route/config tree сверху вниз, postorder удобен для агрегации валидности детей формы, а BFS может пригодиться для поиска ближайшего видимого пункта в древовидном UI.

Что такое BST и почему он не всегда дает O(log n)?

Короткий ответ

Binary Search Tree хранит меньшие значения в левом поддереве, большие — в правом. Если дерево сбалансировано, поиск, вставка и удаление работают за O(log n).

Полный ответ

Binary Search Tree хранит меньшие значения в левом поддереве, большие — в правом. Если дерево сбалансировано, поиск, вставка и удаление работают за O(log n).

Если добавлять уже отсортированные значения в обычный BST без балансировки, дерево может выродиться в цепочку, и поиск станет O(n). Поэтому на практике используют самобалансирующиеся деревья или другие структуры.

Frontend-разработчику редко нужно писать BST руками. Важнее понимать идею упорядоченной структуры и почему обычный массив с бинарным поиском, Map или серверный индекс часто практичнее.

Чем adjacency list отличается от adjacency matrix?

Короткий ответ

Adjacency list хранит для каждой вершины список соседей. Он обычно экономнее для sparse graph, где связей мало относительно числа возможных связей.

Полный ответ

Adjacency list хранит для каждой вершины список соседей. Он обычно экономнее для sparse graph, где связей мало относительно числа возможных связей.

Adjacency matrix хранит таблицу n * n, где быстро проверять наличие ребра между двумя вершинами, но память стоит O(n²). Для больших dependency graph это часто слишком дорого.

const dependencies = new Map<string, readonly string[]>([
  ['checkout', ['shared-ui', 'payments']],
  ['payments', ['shared-ui']],
]);

Для большинства frontend-задач adjacency list через Map<string, string[]> читаемее и дешевле.

Когда рекурсию лучше заменить итерацией во frontend-коде?

Короткий ответ

Рекурсия хорошо выражает дерево или вложенную структуру, но глубина call stack ограничена. Если данные могут прийти от API и быть очень глубокими, рекурсивный обход рискует упасть с RangeError.

Полный ответ

Рекурсия хорошо выражает дерево или вложенную структуру, но глубина call stack ограничена. Если данные могут прийти от API и быть очень глубокими, рекурсивный обход рискует упасть с RangeError.

Итерация с явным stack или queue безопаснее для больших структур и позволяет проще разбивать работу на chunks, чтобы не блокировать main thread.

const stack: TreeNode[] = [root];

while (stack.length > 0) {
  const node = stack.pop();

  if (node === undefined) {
    continue;
  }

  processNode(node);
  stack.push(...node.children);
}
Что такое bitwise operations и почему в JavaScript с ними нужно быть осторожным?

Короткий ответ

Bitwise operations работают с битовым представлением числа: &, |, ^, , >, >>>. В JavaScript такие операции приводят number к 32-bit integer, поэтому большие значения, дроби и NaN ведут себя не как обычная арифметика number.

Полный ответ

Bitwise operations работают с битовым представлением числа: &, |, ^, ~, <<, >>, >>>. В JavaScript такие операции приводят number к 32-bit integer, поэтому большие значения, дроби и NaN ведут себя не как обычная арифметика number.

Битовые маски могут пригодиться для компактного набора flags, permissions или низкоуровневой работы с binary data. Но в обычном frontend-бизнес-коде читаемый объект, Set или enum-like union чаще безопаснее и понятнее.

const enum Permission {
  Read = 1 << 0,
  Write = 1 << 1,
}

const canWrite = (permissions: number): boolean => (permissions & Permission.Write) !== 0;
Зачем frontend-разработчику понимать Big O?

Короткий ответ

Big O помогает оценить, как растет стоимость операции при увеличении входных данных. Для frontend это не абстрактная математика: фильтрация таблицы, группировка списка, сортировка на клиенте, поиск выбранных элементов и построение дерева меню могут выполняться на main thread и напрямую влиять на отзывчивость UI.

Полный ответ

Big O помогает оценить, как растет стоимость операции при увеличении входных данных. Для frontend это не абстрактная математика: фильтрация таблицы, группировка списка, сортировка на клиенте, поиск выбранных элементов и построение дерева меню могут выполняться на main thread и напрямую влиять на отзывчивость UI.

Важно помнить, что Big O не заменяет профилирование. O(n) на маленьком массиве может быть незаметен, а O(n log n) с дорогим comparator, большим DOM update или лишними allocation может давать long task. На практике сначала оценивают порядок роста, затем подтверждают проблему Performance panel, профилировщиком Angular или пользовательскими метриками.

Как оценить сложность фильтрации, сортировки и группировки списка на клиенте?

Короткий ответ

Фильтрация одним проходом обычно стоит O(n). Группировка через Map тоже обычно O(n), если lookup по ключу амортизированно константный. Сортировка чаще стоит O(n log n), но реальная стоимость зависит от comparator и размера элементов.

Полный ответ

Фильтрация одним проходом обычно стоит O(n). Группировка через Map тоже обычно O(n), если lookup по ключу амортизированно константный. Сортировка чаще стоит O(n log n), но реальная стоимость зависит от comparator и размера элементов.

const usersByRole = users.reduce((groups, user) => {
  const roleUsers = groups.get(user.role) ?? [];

  roleUsers.push(user);
  groups.set(user.role, roleUsers);

  return groups;
}, new Map<string, User[]>());

Для Angular важно не только вычисление, но и rendering. Даже быстрый алгоритм может привести к лагам, если после него отрисовать тысячи DOM-узлов без virtual scroll, pagination или server-side filtering.

Где во frontend встречаются stack, queue и priority queue?

Короткий ответ

Stack работает по LIFO. Во frontend он встречается как call stack, undo/redo history, breadcrumbs навигации назад, обход дерева DFS без рекурсии.

Полный ответ

Stack работает по LIFO. Во frontend он встречается как call stack, undo/redo history, breadcrumbs навигации назад, обход дерева DFS без рекурсии.

Queue работает по FIFO. Примеры: очередь toast-уведомлений, upload tasks, последовательные save operations через concatMap, message queue между main thread и Web Worker.

Priority queue отдает элемент с наибольшим или наименьшим приоритетом. В UI она нужна редко, но может пригодиться для планирования фоновых задач, обработки событий по важности или алгоритмов графов. В обычном бизнес-коде часто лучше начать с читаемого массива и сортировки, если объем маленький и профиль не показывает проблему.

Что такое binary search и где он может пригодиться во frontend?

Короткий ответ

Binary search ищет значение в отсортированном массиве, каждый шаг отбрасывая половину диапазона. Сложность поиска — O(log n), но данные должны быть отсортированы и доступны по индексу.

Полный ответ

Binary search ищет значение в отсортированном массиве, каждый шаг отбрасывая половину диапазона. Сложность поиска — O(log n), но данные должны быть отсортированы и доступны по индексу.

Во frontend бинарный поиск редко нужен для обычных UI-списков. Он полезнее в больших данных: поиск позиции в timeline, виртуальный скролл с переменной высотой строк, графики, диапазоны дат, nearest point на canvas.

function findFirstGreaterOrEqual(values: readonly number[], target: number): number {
  let left = 0;
  let right = values.length;

  while (left < right) {
    const middle = Math.floor((left + right) / 2);

    if (values[middle] < target) {
      left = middle + 1;
    } else {
      right = middle;
    }
  }

  return left;
}
Чем quicksort, mergesort и heapsort отличаются на уровне идеи?

Короткий ответ

Quicksort выбирает pivot и делит данные на элементы меньше и больше pivot. В среднем он быстрый, но плохой выбор pivot может привести к O(n²).

Полный ответ

Quicksort выбирает pivot и делит данные на элементы меньше и больше pivot. В среднем он быстрый, но плохой выбор pivot может привести к O(n²).

Mergesort делит массив пополам, сортирует части и сливает их. Он стабильно дает O(n log n), но обычно требует дополнительную память O(n).

Heapsort строит heap и последовательно извлекает минимум или максимум. Он работает за O(n log n) и может обходиться малой дополнительной памятью, но часто менее дружелюбен к cache locality и стабильности порядка.

В прикладном JS/TS-коде обычно используют встроенный toSorted() или sort(), а на собеседовании важно понимать trade-offs и уметь написать корректный comparator.

Что такое heap и как он связан с priority queue?

Короткий ответ

Heap — деревообразная структура, где родитель имеет приоритет не ниже или не выше детей. Binary heap обычно хранится в массиве: для индекса i дети находятся около 2 i + 1 и 2 i + 2.

Полный ответ

Heap — деревообразная структура, где родитель имеет приоритет не ниже или не выше детей. Binary heap обычно хранится в массиве: для индекса i дети находятся около 2 * i + 1 и 2 * i + 2.

Priority queue часто реализуют через heap: вставка и извлечение приоритетного элемента стоят O(log n), чтение верхнего элемента — O(1).

Во frontend это может пригодиться для планировщика фоновых задач, графовых алгоритмов или обработки большого потока событий по приоритетам. Для обычного UI-состояния heap обычно избыточен.

Что такое memoization и чем она отличается от caching?

Короткий ответ

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

Полный ответ

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

Caching шире: можно кешировать HTTP-ответы, изображения, compiled templates, computed state или expensive selectors. У кеша появляются вопросы invalidation, TTL, размера и прав доступа.

Во frontend memoization встречается в Angular computed(), selector-ах state management, pure pipes и ручных индексах через Map. Она полезна только если вычисление дорогое или часто повторяется на тех же входах.

Что такое dynamic programming на базовом уровне?

Короткий ответ

Dynamic programming разбивает задачу на пересекающиеся подзадачи и переиспользует их результаты. Обычно это memoization сверху вниз или табличное вычисление снизу вверх.

Полный ответ

Dynamic programming разбивает задачу на пересекающиеся подзадачи и переиспользует их результаты. Обычно это memoization сверху вниз или табличное вычисление снизу вверх.

Для Angular-разработчика важнее не заучивать академические задачи, а узнавать паттерн: если одно и то же производное значение пересчитывается много раз, можно сохранить результат по ключу.

const priceBySku = new Map<string, number>();

function getPrice(sku: string): number {
  const cachedPrice = priceBySku.get(sku);

  if (cachedPrice !== undefined) {
    return cachedPrice;
  }

  const price = calculatePrice(sku);
  priceBySku.set(sku, price);

  return price;
}
Что такое CPU cache и locality of reference?

Короткий ответ

CPU cache — быстрая память рядом с процессором. Она отличается от browser cache и HTTP cache: CPU cache ускоряет доступ к данным в памяти во время вычислений, а browser/HTTP cache уменьшает сетевые загрузки и чтение ресурсов.

Полный ответ

CPU cache — быстрая память рядом с процессором. Она отличается от browser cache и HTTP cache: CPU cache ускоряет доступ к данным в памяти во время вычислений, а browser/HTTP cache уменьшает сетевые загрузки и чтение ресурсов.

Locality of reference означает, что программа часто обращается к близким участкам памяти или повторно использует недавно прочитанные данные. Последовательный обход массива обычно дружелюбнее к CPU cache, чем хаотичные переходы по ссылкам.

Во frontend это редко оптимизируют руками. Но понимание помогает объяснить, почему большие CPU-bound вычисления лучше измерять, упрощать алгоритмически, переносить в Web Worker или выполнять на сервере.

Практические задачи по алгоритмам и design primitives

Как спроектировать debounce и throttle как reusable utility?

Короткий ответ

Debounce откладывает вызов до паузы в событиях, throttle ограничивает частоту вызовов. Для reusable utility важно заранее определить API: leading/trailing вызовы, cancel, flush, сохранение this, аргументы последнего вызова и тип возвращаемого значения.

Полный ответ

Debounce откладывает вызов до паузы в событиях, throttle ограничивает частоту вызовов. Для reusable utility важно заранее определить API: leading/trailing вызовы, cancel, flush, сохранение this, аргументы последнего вызова и тип возвращаемого значения.

На frontend-интервью полезно привести примеры:

  • debounce - search input, autosave, resize после остановки;
  • throttle - scroll/drag metrics, pointer move, progress updates.

Частая ошибка - использовать одну технику для всех сценариев. Для network search debounce обычно лучше, а для scroll position чаще нужен throttle или requestAnimationFrame.

Реализуйте LRU cache.

Короткий ответ

Что проверяет: Map, порядок доступа, сложность O(1).

Полный ответ

Что проверяет: Map, порядок доступа, сложность O(1).

В JavaScript Map хранит порядок вставки, поэтому простую LRU cache можно реализовать через удаление и повторную вставку ключа при чтении.

class LruCache<TKey, TValue> {
  private readonly values = new Map<TKey, TValue>();

  constructor(private readonly capacity: number) {}

  get(key: TKey): TValue | undefined {
    if (!this.values.has(key)) {
      return undefined;
    }

    const value = this.values.get(key);

    if (value === undefined) {
      return undefined;
    }

    this.values.delete(key);
    this.values.set(key, value);

    return value;
  }

  set(key: TKey, value: TValue): void {
    if (this.values.has(key)) {
      this.values.delete(key);
    }

    this.values.set(key, value);

    if (this.values.size > this.capacity) {
      const oldestKey = this.values.keys().next().value;

      if (oldestKey !== undefined) {
        this.values.delete(oldestKey);
      }
    }
  }
}

На интервью стоит обсудить, почему undefined как значение усложняет get, как обработать capacity <= 0, и когда нужен doubly linked list вместо reliance on Map order.

Что такое пользовательский тип данных

Короткий ответ

Пользовательский тип описывает доменную модель приложения с помощью type, interface, класса, enum или их комбинации.

Полный ответ

Пользовательский тип описывает доменную модель приложения с помощью type, interface, класса, enum или их комбинации.

type UserId = string;

interface User {
  readonly id: UserId;
  readonly name: string;
  readonly role: 'admin' | 'user';
}

Хороший тип выражает ограничения предметной области и делает недопустимые состояния трудными для представления. Для вариантов состояния удобно использовать discriminated union, а для runtime-поведения и DI — классы.

Что такое Union Type (тип объединения) и для чего используется?

Короткий ответ

Union type означает, что значение может принадлежать одному из нескольких типов:

Полный ответ

Union type означает, что значение может принадлежать одному из нескольких типов:

type RequestState<T> =
  | {status: 'idle'}
  | {status: 'loading'}
  | {status: 'success'; data: T}
  | {status: 'error'; error: string};

Перед использованием специфичных свойств union нужно сузить тип через typeof, instanceof, оператор in, проверку discriminant-поля или type guard.

Discriminated union часто лучше набора независимых boolean-флагов: он не позволяет одновременно представить несовместимые состояния, например loading и success.

Что такое декоратор и какие виды декораторов вы знаете?

Короткий ответ

Декоратор — способ добавления метаданных к объявлению класса. Это специальный вид объявления, который может быть присоединен к объявлению класса, методу, методу доступа, свойству или параметру.

Полный ответ

Декоратор — способ добавления метаданных к объявлению класса. Это специальный вид объявления, который может быть присоединен к объявлению класса, методу, методу доступа, свойству или параметру.

Декораторы используют форму @expression, где expression - функция, которая будет вызываться во время выполнения с информацией о декорированном объявлении.

И, чтобы написать собственный декоратор, нам нужно сделать его factory и определить тип:

  • ClassDecorator
  • PropertyDecorator
  • MethodDecorator
  • ParameterDecorator

Декоратор класса

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

export function logClass(target: Function) {
  // Сохранение ссылки на оригинальный конструктор
  const original = target;

  // Функция генерирует экземпляры класса
  function construct(constructor, args) {
    const c: any = function () {
      return constructor.apply(this, args);
    };
    c.prototype = constructor.prototype;
    return new c();
  }

  // Определение поведения нового конструктора
  const f: any = function (...args) {
    console.log(`New: ${original['name']} is created`);
    //New: Employee создан
    return construct(original, args);
  };

  // Копирование прототипа, чтобы оператор intanceof работал
  f.prototype = original.prototype;

  // Возвращает новый конструктор, переписывающий оригинальный
  return f;
}

@logClass
class Employee {}

let emp = new Employee();
console.log('emp instanceof Employee');
//emp instanceof Employee
console.log(emp instanceof Employee);
//true

Декоратор свойства

Объявляется непосредственно перед объявлением метода. Будет вызываться как функция во время выполнения со следующими двумя аргументами:

  • target - прототип текущего объекта, т.е. если Employee является объектом, Employee.prototype
  • propertyKey - название свойства
function logParameter(target: Object, propertyName: string) {
  // Значение свойства
  let _val = this[propertyName];

  // Геттер свойства
  const getter = () => {
    console.log(`Get: ${propertyName} => ${_val}`);
    return _val;
  };

  // Сеттер свойства
  const setter = (newVal) => {
    console.log(`Set: ${propertyName} => ${newVal}`);
    _val = newVal;
  };

  // Удаление свойства
  if (delete this[propertyName]) {
    // Создает новое свойство с геттером и сеттером
    Object.defineProperty(target, propertyName, {
      get: getter,
      set: setter,
      enumerable: true,
      configurable: true,
    });
  }
}

class Employee {
  @logParameter
  name: string;
}

const emp = new Employee();
emp.name = 'Mohan Ram';
console.log(emp.name);

// Set: name => Mohan Ram
// Get: name => Mohan Ram
// Mohan Ram

Декоратор метода

Объявляется непосредственно перед объявлением метода. Будет вызываться как функция во время выполнения со следующими двумя аргументами:

  • target - прототип текущего объекта, т.е. если Employee является объектом, Employee.prototype

  • propertyName - название свойства

  • descriptor - дескриптор свойства метода т.е. - Object.getOwnPropertyDescriptor (Employee.prototype, propertyName)

    export function logMethod(
      target: Object,
      propertyName: string,
      propertyDescriptor: PropertyDescriptor,
    ): PropertyDescriptor {
      const method = propertyDescriptor.value;
    
      propertyDescriptor.value = function (...args: any[]) {
        // Конвертация списка аргументов greet в строку
        const params = args.map((a) => JSON.stringify(a)).join();
    
        // Вызов greet() и получение вернувшегося значения
        const result = method.apply(this, args);
    
        // Конвертация результата в строку
        const r = JSON.stringify(result);
    
        // Отображение в консоли деталей вызова
        console.log(`Call: ${propertyName}(${params}) => ${r}`);
    
        // Возвращение результата вызова
        return result;
      };
      return propertyDescriptor;
    }
    
    class Employee {
      constructor(
        private firstName: string,
        private lastName: string,
      ) {}
    
      @logMethod
      greet(message: string): string {
        return `${this.firstName} ${this.lastName} says: ${message}`;
      }
    }
    
    const emp = new Employee('Mohan Ram', 'Ratnakumar');
    emp.greet('hello');
    //Call: greet("hello") => "Mohan Ram Ratnakumar says: hello"

Декоратор параметра

Объявляется непосредственно перед объявлением метода. Будет вызываться как функция во время выполнения со следующими двумя аргументами:

  • target - прототип текущего объекта, т.е. если Employee является объектом, Employee.prototype
  • propertyKey - название свойства
  • index - индекс параметра в массиве аргументов
function logParameter(target: Object, propertyName: string, index: number) {
  // Генерация метаданных для соответствующего метода
  // для сохранения позиции декорированных параметров
  const metadataKey = `log_${propertyName}_parameters`;

  if (Array.isArray(target[metadataKey])) {
    target[metadataKey].push(index);
  } else {
    target[metadataKey] = [index];
  }
}

class Employee {
  greet(@logParameter message: string): void {
    console.log(`hello ${message}`);
  }
}
const emp = new Employee();
emp.greet('world');
Зачем нам нужны определения типов, где есть JavaScript c динамической типизацией?

Короткий ответ

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

Полный ответ

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

  • неправильное имя свойства;
  • передача аргумента неверного типа;
  • забытая обработка null;
  • несовместимое изменение публичного API.

TypeScript добавляет статический анализ, автодополнение, безопасный рефакторинг и явные контракты между частями приложения. Типы не заменяют runtime-валидацию: данные от API, пользователя и внешних систем все равно считаются недоверенными и должны проверяться.

После компиляции большинство типов удаляется, а браузер выполняет обычный JavaScript.

Поддерживает ли TypeScript перегрузку методов?

Короткий ответ

Да. TypeScript поддерживает несколько сигнатур перегрузки и одну общую реализацию.

Полный ответ

Да. TypeScript поддерживает несколько сигнатур перегрузки и одну общую реализацию.

function format(value: number): string;
function format(value: Date): string;
function format(value: number | Date): string {
  return value instanceof Date ? value.toISOString() : value.toFixed(2);
}

Сигнатура реализации не видна вызывающему коду и должна быть совместима со всеми перегрузками. В runtime существует только одна JavaScript-функция, поэтому различение вариантов выполняет сама реализация.

Если union-параметр дает такой же понятный API, обычно он проще перегрузок.

Возможна ли перегрузка конструктора в TypeScript?

Короткий ответ

Да, с тем же ограничением: можно описать несколько сигнатур, но реализация конструктора остается одна.

Полный ответ

Да, с тем же ограничением: можно описать несколько сигнатур, но реализация конструктора остается одна.

class Point {
  readonly x: number;
  readonly y: number;

  constructor();
  constructor(x: number, y: number);
  constructor(x = 0, y = 0) {
    this.x = x;
    this.y = y;
  }
}

Нельзя написать несколько тел constructor, как в некоторых языках. При большом числе вариантов часто понятнее использовать именованные фабричные методы.

Поддерживает ли TypeScript перегрузку методов (конструкторов)?

Короткий ответ

TypeScript поддерживает перегрузку функций, методов и конструкторов на уровне типов. Сначала объявляются доступные вызывающему коду сигнатуры, затем одна совместимая реализация.

Полный ответ

TypeScript поддерживает перегрузку функций, методов и конструкторов на уровне типов. Сначала объявляются доступные вызывающему коду сигнатуры, затем одна совместимая реализация.

В скомпилированном JavaScript остается одна функция или один конструктор. Поэтому перегрузка не выбирает разные реализации автоматически: код должен сам сузить аргументы.

Перегрузки нужны, когда разные наборы аргументов дают разные, точно связанные возвращаемые типы. Для простых случаев предпочтительнее union types, optional-параметры или объект параметров.

Продвинутый TypeScript

Чем type отличается от interface и что такое intersection type?

Короткий ответ

interface описывает форму объекта, поддерживает declaration merging и удобно расширяется через extends. type может описывать не только объект, но и union, tuple, primitive alias, mapped или conditional type.

Полный ответ

interface описывает форму объекта, поддерживает declaration merging и удобно расширяется через extends. type может описывать не только объект, но и union, tuple, primitive alias, mapped или conditional type.

interface Identifiable {
  readonly id: string;
}

type Timestamped = {
  readonly createdAt: Date;
};

type Entity = Identifiable & Timestamped;

Intersection A & B требует одновременно выполнить оба контракта. Для публичных объектных контрактов часто выбирают interface, для композиции и type-level вычислений — type.

Как типизировать состояние, API response и конфигурацию Angular-компонента?

Короткий ответ

Для состояний удобен discriminated union:

Полный ответ

Для состояний удобен discriminated union:

type LoadState<T> =
  | {status: 'idle'}
  | {status: 'loading'}
  | {status: 'success'; data: T}
  | {status: 'error'; error: string};

API DTO отделяют от доменной модели и преобразуют на data-access границе. Inputs типизируют максимально узко:

readonly user = input.required<Pick<User, "id" | "name">>();

Конфигурации проверяют через satisfies, readonly properties и explicit defaults. Generic-компонент оправдан, когда тип элемента должен проходить через inputs, templates и outputs без потери связи.

Как типами описать дерево в TypeScript?

Короткий ответ

Для дерева обычно описывают узел с payload и дочерними узлами. Если структура readonly для потребителей, это стоит отразить в типе:

Полный ответ

Для дерева обычно описывают узел с payload и дочерними узлами. Если структура readonly для потребителей, это стоит отразить в типе:

interface TreeNode<T> {
  readonly value: T;
  readonly children: ReadonlyArray<TreeNode<T>>;
}

Такой тип подходит для меню, router-like конфигурации, дерева категорий или результата парсинга. Если у узлов бывают разные виды, лучше использовать discriminated union.

type FormNode =
  | {readonly kind: 'group'; readonly controls: ReadonlyArray<FormNode>}
  | {readonly kind: 'field'; readonly name: string; readonly value: string};

Discriminant kind делает обход безопаснее: TypeScript сузит тип в switch и подскажет доступные поля.

Зачем использовать readonly-типы для структур данных?

Короткий ответ

readonly и ReadonlyArray показывают, что вызывающий код не должен менять структуру напрямую. Это особенно полезно для Angular inputs, signals, store state и derived data.

Полный ответ

readonly и ReadonlyArray<T> показывают, что вызывающий код не должен менять структуру напрямую. Это особенно полезно для Angular inputs, signals, store state и derived data.

interface TableState {
  readonly rows: ReadonlyArray<Row>;
  readonly selectedIds: ReadonlySet<string>;
}

Readonly-тип не делает данные глубоко immutable в runtime, но улучшает контракт и снижает риск случайной мутации. Обновление состояния лучше выражать созданием новой структуры:

const nextRows = state.rows.toSorted((first, second) => first.name.localeCompare(second.name));
Что такое generics, generic constraints и keyof?

Короткий ответ

Generic позволяет сохранить связь между входными и выходными типами:

Полный ответ

Generic позволяет сохранить связь между входными и выходными типами:

function getProperty<T extends object, K extends keyof T>(value: T, key: K): T[K] {
  return value[key];
}

T extends object — constraint, ограничивающий допустимые типы. keyof T создает union ключей объекта, а T[K] получает тип конкретного свойства.

Generics нужны для reusable API, но не должны превращать простой код в сложную type-level программу.

Что такое mapped, conditional types и infer?

Короткий ответ

Mapped type преобразует свойства существующего типа:

Полный ответ

Mapped type преобразует свойства существующего типа:

type ReadonlyState<T> = {
  readonly [K in keyof T]: T[K];
};

Conditional type выбирает тип по условию:

type ApiResult<T> = T extends Error ? {error: T} : {data: T};

infer извлекает часть типа внутри conditional type:

type AwaitedValue<T> = T extends Promise<infer Value> ? Value : T;

В прикладном коде сначала используют стандартные utility types: Pick, Omit, Partial, Required, Record, Parameters, ReturnType, Awaited.

Чем satisfies отличается от as?

Короткий ответ

satisfies проверяет совместимость значения с типом, сохраняя максимально точный выведенный тип:

Полный ответ

satisfies проверяет совместимость значения с типом, сохраняя максимально точный выведенный тип:

const routes = {
  home: '/',
  profile: '/profile',
} satisfies Record<string, `/${string}`>;

as утверждает тип и может скрыть ошибку:

const config = value as AppConfig;

Для конфигураций, route maps и provider options предпочтителен satisfies. Type assertion используют только после реального runtime narrowing или на узкой границе interop.

Почему unknown безопаснее any и как писать type guards?

Короткий ответ

any отключает проверку типов и распространяет небезопасность по коду. unknown требует сначала доказать форму значения.

Полный ответ

any отключает проверку типов и распространяет небезопасность по коду. unknown требует сначала доказать форму значения.

function isUser(value: unknown): value is User {
  if (typeof value !== 'object' || value === null) {
    return false;
  }

  return 'id' in value && 'name' in value;
}

Type guard с предикатом value is User сужает тип. Данные API нужно валидировать в runtime: TypeScript не проверяет JSON после загрузки.

Как типизировать граф или dependency graph?

Короткий ответ

Для adjacency list удобно использовать ReadonlyMap или Record, если ключи строковые и данные приходят из JSON.

Полный ответ

Для adjacency list удобно использовать ReadonlyMap или Record, если ключи строковые и данные приходят из JSON.

type ProjectName = string;

type DependencyGraph = ReadonlyMap<ProjectName, ReadonlyArray<ProjectName>>;

Если нужно хранить дополнительные данные о ребре, вводят отдельный тип:

interface DependencyEdge {
  readonly from: ProjectName;
  readonly to: ProjectName;
  readonly type: 'static' | 'dynamic';
}

В frontend такие типы встречаются в визуализации зависимостей, build tooling, state machines и flows навигации. Типы фиксируют форму данных, но cycle detection и валидация внешнего JSON все равно остаются runtime-логикой.

Когда generic data structure оправдана?

Короткий ответ

Generic-структура оправдана, когда один алгоритм действительно работает с разными типами значений и сохраняет связь между входом и выходом.

Полный ответ

Generic-структура оправдана, когда один алгоритм действительно работает с разными типами значений и сохраняет связь между входом и выходом.

interface Queue<T> {
  enqueue(value: T): void;
  dequeue(): T | undefined;
  readonly size: number;
}

Если структура нужна только для одного доменного типа, отдельный generic может быть лишним. Например, NotificationQueue с явными полями и правилами приоритета часто понятнее универсальной Queue<T> плюс набор внешних условий.

TypeScript и runtime-контракты

Чем generic constraints отличаются от intersection types?

Короткий ответ

T extends Constraint ограничивает допустимые типы для generic и разрешает обращаться к полям constraint внутри функции. T & Constraint создает новый intersection type, который требует свойства обеих частей у итогового значения.

Полный ответ

T extends Constraint ограничивает допустимые типы для generic и разрешает обращаться к полям constraint внутри функции. T & Constraint создает новый intersection type, который требует свойства обеих частей у итогового значения.

function byId<T extends {readonly id: string}>(items: ReadonlyArray<T>): ReadonlyMap<string, T> {
  return new Map(items.map((item) => [item.id, item]));
}

Constraint говорит: "принимаю любой тип, но у него должен быть id". Intersection чаще используют, когда нужно описать комбинированную форму данных. На интервью важно не заменять constraint на широкое assertion.

Как типизировать тестовые double без any?

Короткий ответ

Для stub обычно достаточно Pick или satisfies, чтобы описать только используемую часть зависимости.

Полный ответ

Для stub обычно достаточно Pick или satisfies, чтобы описать только используемую часть зависимости.

interface UserApi {
  loadUser(id: string): Promise<User>;
  saveUser(user: User): Promise<void>;
}

const userApiStub = {
  loadUser: async () => ({id: '1', name: 'Ada'}),
} satisfies Pick<UserApi, 'loadUser'>;

Так тест не зависит от лишних методов и не теряет типовую проверку. Если mock framework возвращает широкие типы, лучше изолировать unsafe interop в маленьком helper и не распространять any по тестам.

Когда нужны declaration files .d.ts?

Короткий ответ

.d.ts описывает типы для JavaScript-кода, внешнего global API, CSS modules, assets или пакета без собственных типов. Файл не содержит runtime-кода и не должен обещать то, чего нет в реализации.

Полный ответ

.d.ts описывает типы для JavaScript-кода, внешнего global API, CSS modules, assets или пакета без собственных типов. Файл не содержит runtime-кода и не должен обещать то, чего нет в реализации.

declare module '*.module.css' {
  const classes: Readonly<Record<string, string>>;
  export default classes;
}

В библиотеке declaration files являются частью публичного API. Их нужно проверять вместе с build и не использовать для скрытия реальных несовпадений между TypeScript и runtime.

Почему generic type parameter не дает runtime safety?

Короткий ответ

Generic существует только на этапе компиляции и стирается в JavaScript. Если данные приходят из API, T не проверяет форму ответа в runtime.

Полный ответ

Generic существует только на этапе компиляции и стирается в JavaScript. Если данные приходят из API, T не проверяет форму ответа в runtime.

async function loadJson<T>(url: string): Promise<T> {
  const response = await fetch(url);

  return response.json() as Promise<T>;
}

Такой helper удобен, но он доверяет внешним данным. Для важных контрактов нужна runtime validation: schema, hand-written guard или adapter на границе API. Хороший ответ разделяет compile-time типы и проверку данных, которые пришли извне.

Follow-up вопросы:

  • Что происходит с generic после компиляции?
  • Когда достаточно generic, а когда нужна schema validation?
  • Почему as T может создать ложное чувство безопасности?