Перейти к основному содержимому

ОФФЕР ЗА 24 ЧАСА! 🔥 Реальное собеседование на Middle Frontend 2025 [React/TS, livecoding]

· 23 мин. чтения

Сегодня мы разберём собеседование на позицию фронтенд-разработчика уровня мидл, проведённое в формате живого решения задач по JavaScript, TypeScript и React. Кандидат с четырёхлетним опытом работы демонстрировал уверенное владение базовыми алгоритмами и типизацией, а также показал практический подход к построению React-компонентов с учётом последовательных запросов и условного рендеринга. Несмотря на небольшие затруднения с тонкостями синтаксиса и оптимизацией, собеседование прошло в дружелюбной атмосфере и завершилось перспективой получения офера.

Вопрос 1. Какой у вас опыт работы и в каком формате вы работаете сейчас и ранее?

Таймкод: 00:00:59

Ответ собеседника: Правильный. Около 4 лет опыта, сейчас работаю как ИП через вендора, на предыдущем месте работал по ТК.

Правильный ответ:

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

Вопрос 2. Какие зарплатные ожидания вы имеете?

Таймкод: 00:01:20

Ответ собеседника: Правильный. 200 000 рублей на руки.

Правильный ответ:

Вопрос является стандартным организационным и носит переговорный характер. Ответ собеседника прямой и конкретный, что является хорошим подходом. Правильный ответ не требует дополнительного развёрнутого объяснения.

Вопрос 3. По какой причине вы рассматриваете новое место работы?

Таймкод: 00:01:01:50

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

Правильный ответ:

Вопрос направлен на понимание мотивации смены работы. Ответ собеседника честный и логичный — внешние обстоятельства (оптимизация и сокращение внештатных сотрудников) являются понятной и уважительной причиной поиска новой позиции. Правильный ответ не требует дополнительного развёрнутого объяснения.

Вопрос 4. Какой стек технологий вы использовали за все 4 года опыта?

Таймкод: 00:03:34

Ответ собеседника: Правильный. React, TypeScript, также JavaScript.

Правильный ответ:

Вопрос направлен на понимание общего технологического бэкграунда кандидата. Однако стоит отметить, что для позиции Go-разработчика интервьюера, вероятно, ожидается упоминание Go (Golang) как основного языка, а также связанных технологий: баз данных (PostgreSQL, MySQL, Redis), брокеров сообщений (Kafka, RabbitMQ), фреймворков (gin, echo, gRPC), контейнеризации (Docker, Kubernetes), систем мониторинга (Prometheus, Grafana) и CI/CD инструментов. Ответ собеседника описывает преимущественно frontend-стек, что может указывать либо на широкий профиль разработки, либо на то, что кандидат переходит из frontend/backend-разработки в backend на Go. Для полноты ответа рекомендуется дополнительно раскрыть опыт работы именно с Go-экосистемой.

Вопрос 5. Сможете ли вы оперативно выйти на новое место работы?

Таймкод: 00:04:00

Ответ собеседника: Правильный. Да, в самое ближайшее время смогу выйти.

Правильный ответ:

Вопрос является организационным и направлен на уточнение сроков начала работы. Ответ собеседника положительный и демонстрирует готовность к быстрому старту. Правильный ответ не требует дополнительного развёрнутого объяснения.

Вопрос 6. Расскажите о своём опыте работы, задачах и используемых технологиях

Таймкод: 00:11:33

Ответ собеседника: Правильный. Фронтенд-разработчик на React с опытом около 4 лет. Работал над проектами городских парковок и приложением для заказа кредитных карт. Использовал React, TypeScript, React Hook Form, Ant Design, Zod, React Table, SCSS модули. Работал по скраму с трёхнедельными спринтами в команде из 3 фронтендеров, 2 бэкендеров и аналитиков.

Правильный ответ:

Ответ собеседника достаточно подробный и структурированный — указан стек технологий, типы проектов, размер команды и процесс разработки. Однако для позиции Go-разработчика данный ответ описывает исключительно frontend-опыт. Если кандидат претендует на backend-позицию на Go, рекомендуется дополнительно раскрыть:

  • Опыт работы с Go (если есть): в каких проектах использовал, какие фреймворки и библиотеки применял.
  • Опыт работы с базами данных: PostgreSQL, MySQL, MongoDB, Redis — написание запросов, проектирование схем, оптимизация.
  • Опыт работы с API: REST, gRPC, GraphQL — проектирование и реализация.
  • Опыт работы с инфраструктурой: Docker, Kubernetes, CI/CD, мониторинг.
  • Понимание микросервисной архитектуры, паттернов проектирования, принципов SOLID в контексте Go.

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

Вопрос 7. Было ли в вашей команде ревью кода и как оно организовано?

Таймкод: 00:13:17

Ответ собеседника: Правильный. Да, ревью есть. Изначально требовалось 2 апрува, но из-за того, что некоторые разработчики неохотно ревьюили, количество увеличили до 3 апрувов.

Правильный ответ:

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

  • Инструменты для ревью: использование GitHub Pull Requests, GitLab Merge Requests, Gerrit, Bitbucket.
  • Критерии ревью: проверка соответствия кодстайлу (gofmt, golint, staticcheck), корректность обработки ошибок, отсутствие race conditions, корректная работа с конкурентностью (goroutines, channels, mutexes).
  • Процесс: описание жизненного цикла — от создания MR до мержа, кто назначается ревьюером, есть ли автоматические проверки (CI pipeline, линтеры, тесты).
  • Best practices: упоминание практик, таких как небольшие атомарные MR, описательные коммиты, конструктивная обратная связь.

Увеличение количества апрувов до 3 из-за неохоты разработчиков — это симптом проблемы с процессом, и было бы полезнее упомянуть, как команда решала эту проблему на уровне процессов (например, ротация ревьюеров, выделенное время на ревью).

Вопрос 8. Расскажите о сложной или интересной задаче, которую вы решали

Таймкод: 00:13:43

Ответ собеседника: Правильный. Реализовывал динамическую генерацию формы на основе выбора в первом селекте. Форма для скачивания файлов разных форматов (PDF, Excel и др.), где в зависимости от выбранного формата отрисовывались дополнительные селекты с настройками. Структура приходила с бэкенда, нужно было нормализовать данные, настроить React Hook Form и валидацию.

Правильный ответ:

Ответ собеседника демонстрирует опыт работы с динамическими формами, нормализацией данных и клиентской валидацией. Для позиции Go-разработчика было бы более релевантно привести пример backend-задачи. Примеры сложных backend-задач на Go:

  • Разработка высоконагруженного API: реализация REST или gRPC сервиса, обрабатывающего тысячи запросов в секунду, с использованием пула воркеров, rate limiting, кэширования.
  • Оптимизация работы с базой данных: переписывание медленных SQL-запросов, внедрение индексов, репликации, шардирования.
  • Интеграция с брокерами сообщений: реализация продюсеров и консьюмеров для Kafka/RabbitMQ, обработка сообщений с гарантией exactly-once доставки.
  • Миграция монолита на микросервисы: выделение отдельных сервисов, настройка межсервисного взаимодействия, внедрение service discovery.
  • Реализация системы авторизации: JWT, OAuth2, распределённые сессии.

Для полноты ответа рекомендуется описать конкретную backend-задачу, технологии Go, которые использовались, и достигнутый результат.

Вопрос 9. Как вы пришли к использованию ленивой загрузки (lazy loading) и какие инструменты использовали для анализа производительности?

Таймкод: 00:15:51

Ответ собеседника: Неполный. Lazy loading используется для разбиения приложения на чанки, чтобы уменьшить объём данных для отображения конкретного модуля. В команде выделяется 20% времени на технические задачи, в том числе рефакторинг. Для анализа использовал DevTools, вкладку Performance и Lighthouse, но не проводил точных измерений в цифрах, а сравнивал результаты до и после изменений.

Правильный ответ:

Lazy Loading в контексте веб-приложений

Lazy loading (ленивая загрузка) — это паттерн, при котором загрузка ресурсов или компонентов откладывается до момента, когда они действительно необходимы. В контексте React-приложений это реализуется через React.lazy() и Suspense для динамического импорта компонентов, что позволяет разбить бандл на чанки и уменьшить время начальной загрузки страницы.

const LazyComponent = React.lazy(() => import('./LazyComponent'));

function App() {
return (
<Suspense fallback={<div>Loading...</div>}>
<LazyComponent />
</Suspense>
);
}

Анализ производительности

Для анализа производительности веб-приложений используются следующие инструменты:

  • Chrome DevTools — вкладка Performance: позволяет записывать и анализировать производительность рендеринга, выявлять узкие места в выполнении JavaScript, отслеживать layout shifts и repaints.
  • Lighthouse: автоматизированный инструмент для аудита производительности, доступности, SEO и лучших практик. Выдаёт метрики: First Contentful Paint (FCP), Largest Contentful Paint (LCP), Time to Interactive (TTI), Cumulative Layout Shift (CLS).
  • WebPageTest: позволяет проводить тестирование загрузки страницы из разных географических точек и с разными условиями сети.
  • Bundle Analyzer (webpack-bundle-analyzer): визуализирует состав бандла, помогает выявить тяжёлые зависимости и оптимизировать размер чанков.

Lazy Loading и оптимизация на бэкенде (Go)

Для позиции Go-разработчика также важно понимать аналоги lazy loading на серверной стороне:

  • Пагинация в базе данных: загрузка данных порциями через LIMIT и OFFSET или курсорную пагинацию.
  • Ленивые вычисления: использование каналов и горутин для отложенной обработки данных.
  • Кэширование: применение Redis или in-memory кэшей для уменьшения нагрузки на базу данных.

Ответ собеседника можно дополнить конкретными цифрами улучшений (например, «размер бандла уменьшился на 40%», «FCP улучшился с 3.2с до 1.8с»), что усилит демонстрацию результативности работы.

Вопрос 10. Как вы работали с JWT токенами на предыдущем месте работы?

Таймкод: 00:19:00

Ответ собеседника: Правильный. Использовали классическую схему с refresh token и access token. Один токен хранили в cookies, другой в state/Redux.

Правильный ответ:

Ответ собеседника демонстрирует базовое понимание работы с JWT. Для более полного ответа на позицию Go-разработчика рекомендуется раскрыть серверную часть работы с JWT.

Схема Access Token + Refresh Token

  • Access Token — короткоживущий токен (обычно 15–30 минут), используется для авторизации запросов к API.
  • Refresh Token — долгоживущий токен (часы, дни, недели), используется для получения нового access token без повторной аутентификации.

Безопасное хранение токенов

  • Access Token: хранение в памяти (state, Redux) или в httpOnly cookie. Хранение в localStorage уязвимо к XSS-атакам.
  • Refresh Token: предпочтительно хранение в httpOnly, Secure, SameSite=Strict cookie для защиты от XSS и CSRF атак. Альтернативно — хранение в базе данных на сервере с возможностью отзыва.

Реализация JWT на Go

package main

import (
"fmt"
"time"
"github.com/golang-jwt/jwt/v5"
)

var jwtSecret = []byte("your-secret-key")

type Claims struct {
UserID uint `json:"user_id"`
Role string `json:"role"`
jwt.RegisteredClaims
}

func GenerateAccessToken(userID uint, role string) (string, error) {
claims := &Claims{
UserID: userID,
Role: role,
RegisteredClaims: jwt.RegisteredClaims{
ExpiresAt: jwt.NewNumericDate(time.Now().Add(15 * time.Minute)),
IssuedAt: jwt.NewNumericDate(time.Now()),
Issuer: "my-service",
},
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
return token.SignedString(jwtSecret)
}

func ValidateToken(tokenString string) (*Claims, error) {
token, err := jwt.ParseWithClaims(tokenString, &Claims{}, func(token *jwt.Token) (interface{}, error) {
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
}
return jwtSecret, nil
})
if err != nil {
return nil, err
}
if claims, ok := token.Claims.(*Claims); ok && token.Valid {
return claims, nil
}
return nil, fmt.Errorf("invalid token")
}

Ключевые аспекты безопасности

  • Использование сильного секретного ключа или асимметричных ключей (RS256).
  • Установка короткого времени жизни access token.
  • Реализация механизма отзыва refresh token (token blacklist, хранение в Redis).
  • Защита от CSRF при использовании cookies.
  • Передача токена в заголовке Authorization: Bearer <token>.
  • Хранение секрета в переменных окружения или секрет-менеджерах (Vault, AWS Secrets Manager).

Вопрос 11. Реализуйте функцию, которая посчитает количество гласных в строке

Таймкод: 00:19:50

Ответ собеседника: Правильный. Решение с использованием reduce, Set для гласных и spread оператора для преобразования строки в массив. Сложность O(n), так как проходим по строке один раз, а has у Set работает за O(1).

Правильный ответ:

Ответ собеседника корректен — он описал решение с оптимальной временной сложностью O(n). Вот реализация на Go с несколькими вариантами:

Вариант 1: Использование map для проверки гласных

package main

import (
"fmt"
"strings"
)

func countVowels(s string) int {
vowels := map[rune]bool{
'a': true, 'e': true, 'i': true, 'o': true, 'u': true,
'A': true, 'E': true, 'I': true, 'O': true, 'U': true,
}
count := 0
for _, ch := range s {
if vowels[ch] {
count++
}
}
return count
}

func main() {
fmt.Println(countVowels("Hello World")) // 3
fmt.Println(countVowels("AEIOU")) // 5
fmt.Println(countVowels("bcdfg")) // 0
}

Вариант 2: Использование strings.ContainsRune

func countVowels(s string) int {
count := 0
for _, ch := range s {
if strings.ContainsRune("aeiouAEIOU", ch) {
count++
}
}
return count
}

Вариант 3: Подсчёт с учётом кириллицы

func countVowels(s string) int {
vowels := "aeiouAEIOUаеёиоуыэюяАЕЁИОУЫЭЮЯ"
count := 0
for _, ch := range s {
if strings.ContainsRune(vowels, ch) {
count++
}
}
return count
}

Анализ сложности

  • Временная сложность: O(n), где n — длина строки. Каждый символ провроверяется ровно один раз.
  • Пространственная сложность: O(1) — используется фиксированное количество памяти для хранения множества гласных (map или строка константного размера).

Использование rune вместо byte важно для корректной работы с Unicode-символами, включая кириллицу и другие многобайтовые символы.

Вопрос 12. Объясните разницу между spread оператором и split для преобразования строки в массив

Таймкод: 00:25:53

Ответ собеседника: Неполный. Spread применяется к итерируемым объектам, строка является итерируемым объектом. Split вызывает метод строки. По производительности spread медленнее примерно в 3 раза на больших данных. Spread может обработать юникодные символы (суррогатные пары), а split нет.

Правильный ответ:

Ответ собеседника в целом верный, но требует дополнения и уточнений.

Spread оператор (...)

Spread оператор использует протокол итерации (Symbol.iterator) строки. Строка в JavaScript является итерируемым объектом, и spread вызывает её встроенный итератор, который корректно обрабатывает Unicode-символы, включая суррогатные пары (эмодзи, иероглифы и т.д.).

const str = "Hello";
const arr = [...str]; // ['H', 'e', 'l', 'l', 'o']

const emoji = "😀🎉";
const arrEmoji = [...emoji]; // ['😀', '🎉']

String.prototype.split('')

Метод split('') разбивает строку по пустому разделителю, работая с внутренним представлением строки как последовательности UTF-16 code units. Это может приводить к некорректной обработке символов, представленных суррогатными парами.

const str = "Hello";
const arr = str.split(''); // ['H', 'e', 'l', 'l', 'o']

const emoji = "😀🎉";
const arrEmoji = emoji.split(''); // ['\uD83D', '\uDE00', '\uD83C', '\uDF89']
// Эмодзи разбиты на отдельные суррогаты — некорректно

Ключевые различия

ХарактеристикаSpread (...)split('')
МеханизмSymbol.iteratorРазбиение по разделителю
Unicode (суррогатные пары)Корректная обработкаРазбивает суррогатные пары
ПроизводительностьНемного медленнееБыстрее
ПрименимостьЛюбые итерируемые объектыТолько строки
ПамятьСоздаёт новый массивСоздаёт новый массил

Аналогия в Go

В Go аналогом разбиения строки на символы является итерация по rune, что корректно обрабатывает Unicode:

package main

import "fmt"

func main() {
str := "Hello 😀"

// Корректная итерация по символам (rune)
runes := []rune(str)
fmt.Printf("%c\n", runes) // [H e l l o 😀]

// Некорректная итерация по байтам
bytes := []byte(str)
fmt.Println(bytes) // байтовое представление
}

Таким образом, spread в JavaScript ближе к итерации по rune в Go, а split('') — к итерации по byte. Для корректной работы с Unicode предпочтительнее использовать spread или Array.from(str).

Вопрос 13. Напишите функцию, которая принимает объект любой вложенности и строку с ключами через точку, и возвращает значение по указанному пути или undefined

Таймкод: 00:27:24

Ответ собеседника: Правильный. Решение с использованием split для преобразования строки пути в массив ключей, затем цикл for...of для итерации по ключам и проверки наличия каждого ключа в текущем объекте. Если ключ существует, переходим к следующему уровню вложенности. В конце возвращаем текущее значение или undefined.

Правильный ответ:

Ответ собеседника корректен по алгоритму. Вот реализация на JavaScript и аналог на Go.

Реализация на JavaScript

function getValueByPath(obj, path) {
const keys = path.split('.');
let current = obj;

for (const key of keys) {
if (current === undefined || current === null || typeof current !== 'object') {
return undefined;
}
current = current[key];
}

return current;
}

// Примеры использования
const data = {
user: {
profile: {
name: "John",
address: {
city: "Moscow"
}
}
}
};

console.log(getValueByPath(data, "user.profile.name")); // "John"
console.log(getValueByPath(data, "user.profile.address.city")); // "Moscow"
console.log(getValueByPath(data, "user.profile.age")); // undefined
console.log(getValueByPath(data, "user.settings.theme")); // undefined

Реализация на Go

package main

import (
"fmt"
"strings"
)

func getValueByPath(obj map[string]interface{}, path string) (interface{}, bool) {
keys := strings.Split(path, ".")
current := obj

for i, key := range keys {
val, exists := current[key]
if !exists {
return nil, false
}

// Если это последний ключ — возвращаем значение
if i == len(keys)-1 {
return val, true
}

// Пытаемся привести к следующему уровню вложенности
next, ok := val.(map[string]interface{})
if !ok {
return nil, false
}
current = next
}

return nil, false
}

func main() {
data := map[string]interface{}{
"user": map[string]interface{}{
"profile": map[string]interface{}{
"name": "John",
"address": map[string]interface{}{
"city": "Moscow",
},
},
},
}

val, ok := getValueByPath(data, "user.profile.name")
if ok {
fmt.Println(val) // John
}

val, ok = getValueByPath(data, "user.profile.age")
if !ok {
fmt.Println("not found") // not found
}
}

Ключевые моменты

  • Разделение пути: строка "user.profile.name" разбивается на массив ["user", "profile", "name"].
  • Итерация: на каждом шаге проверяется существование ключа в текущем уровне объекта.
  • Проверка типа: в Go необходимо явное приведение типа (val.(map[string]interface{})), так как map[string]interface{} — это не вложенный map, а значение типа interface{}.
  • Edge cases: пустой путь, null/undefined значения на пути, примитивные значения вместо объектов на промежуточных уровнях.
  • Временная сложность: O(k), где k — количество ключей в пути.

Вопрос 14. Почему в цикле for...of использовали let вместо const для переменной key?

Таймкод: 00:32:44

Ответ собеседника: Правильный. Это дело привычки. В данном случае на каждой итерации создаётся своя переменная key, поэтому можно использовать и const, и let. Обычно на собеседованиях пишу let, чтобы не было лишних вопросов.

Правильный ответ:

Ответ собеседника корректен. В цикле for...of на каждой итерации создаётся новая переменная, поэтому const и let ведут себя одинаково — переменная не может быть переприсвоена в рамках одной итерации.

// Оба варианта работают корректно
for (const key of keys) {
console.log(key); // OK
}

for (let key of keys) {
console.log(key); // OK
}

Когда const вызовет ошибку в циклах

const вызовет ошибку только в классическом for-цикле, где происходит переприсваивание:

// Ошибка: Assignment to constant variable
for (const i = 0; i < 10; i++) {
console.log(i);
}

// Корректно
for (let i = 0; i < 10; i++) {
console.log(i);
}

Рекомендация

Использование const в for...of является лучшей практикой, так как явно показывает намерение не изменять переменную внутри тела цикла:

for (const key of keys) {
// key не будет переприсвоен — это сигнал читателю кода
current = current[key];
}

Это соответствует принципу минимальных привилегий: используйте const по умолчанию и переключайтесь на let только когда необходимо переприсваивание.

Вопрос 15. Реализуйте type guard функцию isFish, которая проверяет, является ли объект типом Fish

Таймкод: 00:33:48

Ответ собеседника: Правильный. Функция принимает аргумент pet типа Fish | Bird. Используется оператор 'in' для проверки наличия уникального свойства Fish (swim). Возвращается pet is Fish. В блоке после проверки TypeScript понимает, что pet является Fish, и можно обращаться к методу swim.

Правильный ответ:

Ответ собеседника полностью корректен. Вот полная реализация с примерами.

Определение типов и type guard

type Fish = {
swim: () => void;
};

type Bird = {
fly: () => void;
};

// Type guard с использованием оператора 'in'
function isFish(pet: Fish | Bird): pet is Fish {
return 'swim' in pet;
}

// Альтернативная реализация через проверку метода
function isFishAlt(pet: Fish | Bird): pet is Fish {
return (pet as Fish).swim !== undefined;
}

// Использование
function move(pet: Fish | Bird) {
if (isFish(pet)) {
pet.swim(); // TypeScript знает, что здесь pet — Fish
} else {
pet.fly(); // TypeScript знает, что здесь pet — Bird
}
}

const myFish: Fish = { swim: () => console.log("Swimming") };
const myBird: Bird = { fly: () => console.log("Flying") };

move(myFish); // Swimming
move(myBird); // Flying

Type guard с использованием discriminated union

Более идиоматический подход в TypeScript — использование дискриминированного union с общим полем-тегом:

type Fish = {
type: 'fish';
swim: () => void;
};

type Bird = {
type: 'bird';
fly: () => void;
};

function isFish(pet: Fish | Bird): pet is Fish {
return pet.type === 'fish';
}

Аналогия в Go

В Go аналогом type guard является type assertion или type switch:

package main

import "fmt"

type Fish struct{}
func (f Fish) Swim() { fmt.Println("Swimming") }

type Bird struct{}
func (b Bird) Fly() { fmt.Println("Flying") }

func move(pet interface{}) {
switch p := pet.(type) {
case Fish:
p.Swim()
case Bird:
p.Fly()
default:
fmt.Println("Unknown type")
}
}

func main() {
move(Fish{}) // Swimming
move(Bird{}) // Flying
}

Ключевые моменты

  • pet is Fish — это type predicate, который сообщает TypeScript о типе внутри условного блока.
  • Оператор in проверяет наличие свойства в объекте и является безопасным способом различения типов.
  • Type guard позволяет избежать явного приведения типов (as Fish) и делает код более безопасным и читаемым.

Вопрос 16. Сможете ли вы написать утилитарный тип Optional на TypeScript, который делает все поля необязательными?

Таймкод: 00:39:46

Ответ собеседника: Неполный. Кандидат редко использует утилитарные типы и не смог вспомнить синтаксис Optional. Знает, что Optional делает все поля необязательными (противоположность Required), но не смог реализовать его на TypeScript. Просил подсказки, но не смог завершить задачу.

Правильный ответ:

Ответ собеседника неполный — кандидат не смог реализовать тип. Вот полное решение.

Реализация Partial (Optional) типа

type Optional<T> = {
[K in keyof T]?: T[K];
};

// Использование
interface User {
id: number;
name: string;
email: string;
}

type OptionalUser = Optional<User>;
// Эквивалентно:
// {
// id?: number;
// name?: string;
// email?: string;
// }

const partialUser: OptionalUser = { name: "John" }; // OK

Как это работает

  • keyof T — получает объединение всех ключей типа T ("id" | "name" | "email").
  • K in keyof T — mapped type, итерирующийся по каждому ключу.
  • ? — делает свойство необязательным (добавляет undefined к типу).
  • T[K] — сохраняет оригинальный тип значения.

Встроенный тип Partial

В TypeScript этот тип уже встроен и называется Partial<T>:

type Partial<T> = {
[P in keyof T]?: T[P];
};

type OptionalUser = Partial<User>;

Связанные утилитарные типы

// Required — делает все поля обязательными (противоположность Partial)
type Required<T> = {
[K in keyof T]-?: T[K];
};

// Pick — выбирает только указанные поля
type Pick<T, K extends keyof T> = {
[P in K]: T[P];
};

// Omit — исключает указанные поля
type Omit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>;

// Readonly — делает все поля только для чтения
type Readonly<T> = {
readonly [K in keyof T]: T[K];
};

// Record — создаёт тип объекта с указанными ключами и значениями
type Record<K extends keyof any, T> = {
[P in K]: T;
};

Практическое применение

interface User {
id: number;
name: string;
email: string;
}

// Для обновления пользователя — все поля необязательны
function updateUser(id: number, updates: Partial<User>): User {
const existingUser = getUserById(id);
return { ...existingUser, ...updates };
}

// Для DTO создания — исключаем id
type CreateUserDTO = Omit<User, 'id'>;

// Для профиля — только нужные поля
type UserProfile = Pick<User, 'name' | 'email'>;

Аналогия в Go

В Go нет дженериков для типов структур на уровне TypeScript, но можно использовать указатели для опциональных полей:

type User struct {
ID *int `json:"id,omitempty"`
Name *string `json:"name,omitempty"`
Email *string `json:"email,omitempty"`
}

Знание утилитарных типов TypeScript важно для понимания продвинутой типизации и часто проверяется на собеседованиях.

Вопрос 17. Какие методы для оптимизации рендеров компонентов React вы используете?

Таймкод: 00:42:07

Ответ собеседника: Правильный. Использую useMemo для мемоизации. Также упомянул рекомпозицию и использование пропсов для избежания лишних рендеров. Важно не допускать ошибок при использовании useEffect.

Правильный ответ:

Ответ собеседника корректен, но его можно значительно расширить. Вот полный обзор методов оптимизации рендеров в React.

1. Мемоизация компонентов с React.memo

const MyComponent = React.memo(({ name, age }: Props) => {
return <div>{name}: {age}</div>;
});

// С кастомным компаратором
const MyComponent = React.memo(({ name }: Props) => {
return <div>{name}</div>;
}, (prevProps, nextProps) => {
return prevProps.name === nextProps.name;
});

React.memo предотвращает повторный рендер компонента, если его пропсы не изменились.

2. Мемоизация значений с useMemo

const expensiveValue = useMemo(() => {
return computeExpensiveValue(a, b);
}, [a, b]);

useMemo кэширует результат вычисления и пересчитывает его только при изменении зависимостей.

3. Мемоизация колбэков с useCallback

const handleClick = useCallback(() => {
doSomething(id);
}, [id]);

useCallback предотвращает создание новой функции при каждом рендере, что важно при передаче колбэков в React.memo компоненты.

4. Оптимизация через композицию (children)

// Плохо: при рендере Parent перерисовывается Child
function Parent() {
const [count, setCount] = useState(0);
return (
<div>
<button onClick={() => setCount(c => c + 1)}>{count}</button>
<Child /> {/* Перерисовывается при каждом клике */}
</div>
);
}

// Хорошо: children не перерисовывается
function Parent({ children }: { children: React.ReactNode }) {
const [count, setCount] = useState(0);
return (
<div>
<button onClick={() => setCount(c => c + 1)}>{count}</button>
{children}
</div>
);
}

// Использование
<Parent>
<Child /> {/* Не перерисовывается при изменении count */}
</Parent>

5. Ленивая загрузка (React.lazy + Suspense)

const HeavyComponent = React.lazy(() => import('./HeavyComponent'));

function App() {
return (
<Suspense fallback={<div>Loading...</div>}>
<HeavyComponent />
</Suspense>
);
}

6. Виртуализация списков

Для длинных списков используются библиотеки react-window или react-virtualized, которые рендерят только видимые элементы.

import { FixedSizeList } from 'react-window';

const Row = ({ index, style }) => (
<div style={style}>Row {index}</div>
);

const List = () => (
<FixedSizeList height={600} itemCount={1000} itemSize={35}>
{Row}
</FixedSizeList>
);

7. Оптимизация useEffect

  • Указывайте корректный массив зависимостей.
  • Избегайте ненужных вызовов эффектов.
  • Используйте cleanup-функцию для предотвращения утечек памяти.

8. Использование key в списках

Корректные key помогают React точно определять, какие элементы изменились:

// Плохо: индекс как key
{items.map((item, index) => <Item key={index} {...item} />)}

// Хорошо: уникальный идентификатор
{items.map(item => <Item key={item.id} {...item} />)}

Когда НЕ нужно оптимизировать

Преждевременная оптимизация может навредить читаемости кода. Оптимизируйте только тогда, когда есть измеренные проблемы с производительностью, выявленные через React DevTools Profiler или Lighthouse.

Вопрос 18. Напишите компонент UserCard, который принимает userId, загружает данные пользователя и его аватарку, и отображает их

Таймкод: 00:42:53

Ответ собеседника: Правильный. Создал компонент с использованием useState для хранения userData и userPic. Реализовал асинхронную функцию fetchData, которая последовательно вызывает getUserData и getUserPic. Использовал useEffect для вызова fetchData при монтировании. Отрисовал img с src=userPic и alt=userFirstName, а также заголовок с именем пользователя. Добавил условную логику для отображения фамилии в зависимости от пола.

Правильный ответ:

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

Полная реализация компонента

import React, { useState, useEffect } from 'react';

interface User {
id: number;
firstName: string;
lastName: string;
gender: 'male' | 'female';
}

interface UserCardProps {
userId: number;
}

const getUserData = async (id: number): Promise<User> => {
const response = await fetch(`/api/users/${id}`);
if (!response.ok) throw new Error('Failed to fetch user data');
return response.json();
};

const getUserPic = async (id: number): Promise<string> => {
const response = await fetch(`/api/users/${id}/avatar`);
if (!response.ok) throw new Error('Failed to fetch avatar');
const data = await response.json();
return data.url;
};

const UserCard: React.FC<UserCardProps> = ({ userId }) => {
const [userData, setUserData] = useState<User | null>(null);
const [userPic, setUserPic] = useState<string>('');
const [loading, setLoading] = useState<boolean>(true);
const [error, setError] = useState<string | null>(null);

useEffect(() => {
let isMounted = true;

const fetchData = async () => {
try {
setLoading(true);
setError(null);

// Параллельная загрузка данных и аватара
const [user, pic] = await Promise.all([
getUserData(userId),
getUserPic(userId)
]);

if (isMounted) {
setUserData(user);
setUserPic(pic);
}
} catch (err) {
if (isMounted) {
setError(err instanceof Error ? err.message : 'Unknown error');
}
} finally {
if (isMounted) {
setLoading(false);
}
}
};

fetchData();

// Cleanup для предотвращения утечек памяти
return () => {
isMounted = false;
};
}, [userId]);

if (loading) return <div>Loading...</div>;
if (error) return <div>Error: {error}</div>;
if (!userData) return <div>User not found</div>;

return (
<div className="user-card">
<img src={userPic} alt={userData.firstName} />
<h2>{userData.firstName} {userData.lastName}</h2>
{userData.gender === 'female' && <p>Ms. {userData.lastName}</p>}
</div>
);
};

export default UserCard;

Ключевые улучшения

  • Параллельная загрузка: использование Promise.all вместо последовательных вызовов ускоряет загрузку.
  • Обработка ошибок: try/catch с отображением сообщения об ошибке.
  • Состояние загрузки: индикатор загрузки для лучшего UX.
  • Cleanup функция: isMounted предотвращает обновление state после размонтирования компонента (утечка памяти).
  • Зависимость userId в useEffect: данные перезагружаются при изменении userId.

Аналогичный паттерн на Go (HTTP handler)

func getUserCard(w http.ResponseWriter, r *http.Request) {
userID := r.URL.Query().Get("userId")

var user User
var picURL string

// Параллельные запросы
userCh := make(chan User)
picCh := make(chan string)
errCh := make(chan error, 2)

go func() {
user, err := getUserData(userID)
if err != nil {
errCh <- err
return
}
userCh <- user
}()

go func() {
pic, err := getUserPic(userID)
if err != nil {
errCh <- err
return
}
picCh <- pic
}()

// Сбор результатов
user = <-userCh
picURL = <-picCh

// Рендеринг ответа...
}

Вопрос 19. Как вы определяете, какой разработчик пойдёт на какой проект?

Таймкод: 00:57:48

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

Правильный ответ:

Вопрос носит управленческий характер и направлен на понимание подходов к распределению ресурсов. Ответ собеседника корректен и отражает прагматичный подход. Правильный ответ не требует дополнительного развёрнутого объяснения.

Вопрос 20. Используете ли вы AI-инструменты (ChatGPT, Cursor и т.д.) в процессе разработки?

Таймкод: 01:00:36

Ответ собеседника: Правильный. В компании доступен ChatGPT для всех сотрудников. Фронтенд-разработчики используют VS Code с плагином GigaChat для генерации юнит-тестов, оптимизации кода и других задач. Планируется развёртывание внутренних AI-моделей (например, Qwen) внутри контура банка для соответствия требованиям безопасности.

Правильный ответ:

Ответ собеседника демонстрирует осведомлённость о современных AI-инструментах и понимание требований безопасности при их использовании. Правильный ответ не требует дополнительного развёрнутого объяснения.