Gernar
Frontend DeveloperTypeScript

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

Что такое Function Overload в TypeScript

Function Overload описывает несколько вариантов вызова одной функции и помогает вернуть точный тип результата для каждого варианта. Главный риск в ответе: перепутать его с несколькими реализациями или использовать там, где хватит union type.

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

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

🐰0
🥚0

Мини-квиз

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

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

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

Как лучше объяснить Function Overload на интервью?

Вы хотите дать короткое и точное определение.

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

Разбор

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

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

Базовая идея

Перегрузка функции в TypeScript состоит из двух частей: нескольких overload-сигнатур и одной реализации. Overload-сигнатуры описывают, как функцию можно вызвать снаружи. Реализация содержит реальный код и должна обработать все объявленные варианты.

Простой пример:

function parseValue(value: string): string[];
function parseValue(value: number): number;
function parseValue(value: string | number): string[] | number {
  if (typeof value === "string") {
    return value.split(",");
  }

  return value * 100;
}

const tags = parseValue("react,typescript"); // string[]
const percent = parseValue(0.42); // number

Здесь вызывающий код получает разные типы результата. Для строки это string[], для числа это number. В этом практическая ценность overload. Вы не просто разрешаете разные типы входа, а сохраняете точный контракт между входом и выходом.

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

Хороший короткий ответ может звучать так:

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

После этого добавьте практический критерий. Перегрузки нужны не всегда. Если возвращаемый тип не зависит от формы входа, union type часто проще. Если зависит, overload делает API удобнее и уменьшает лишние проверки в вызывающем коде.

Когда выбирать overload, union или generic

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

Как выбрать подход

1Результат одинаковый для всех вариантов входа?
Обычно достаточно union type в параметре.
2Результат меняется в зависимости от конкретной формы вызова?
Рассмотрите overload, чтобы вызывающий код получил точный тип.
3Нужно сохранить тот же тип, который пришел на вход?
Часто лучше подходит generic, а не список перегрузок.
4Форм вызова стало много и они плохо читаются?
Лучше пересмотреть API. Например, принять объект с явными полями.

Пример, где overload не обязателен:

function formatValue(value: string | number): string {
  return String(value);
}

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

Главная ловушка: реализация не является публичной сигнатурой

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

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

function loadUser(id: number): Promise<User>;
function loadUser(slug: string): Promise<User>;
function loadUser(value: number | string | boolean): Promise<User> {
  return fetch(`/api/users/${String(value)}`).then((response) => response.json());
}

loadUser(true); // ошибка типов, потому что такой overload не объявлен

В этом примере широкая реализация опасна не из-за самого overload, а из-за тела функции. Если значение придет из JavaScript-кода, формы или URL без проверки, можно отправить лишний запрос на /api/users/true, получить неверный error или показать в UI пустое состояние вместо понятной ошибки.

Безопаснее держать реализацию настолько широкой, насколько нужно для объявленных перегрузок, и явно проверять аргументы перед сетевым вызовом:

function loadUser(id: number): Promise<User>;
function loadUser(slug: string): Promise<User>;
function loadUser(value: number | string): Promise<User> {
  const path = typeof value === "number" ? `id/${value}` : `slug/${encodeURIComponent(value)}`;

  return fetch(`/api/users/${path}`).then(async (response) => {
    if (!response.ok) {
      throw new Error("User request failed");
    }

    const data: unknown = await response.json();
    return parseUser(data);
  });
}

Здесь parseUser означает вашу проверку формы ответа. Не доверяйте JSON только потому, что overload красиво описал вызов. Не превращайте реализацию в any без причины. И обязательно проверяйте аргументы в теле, потому что в runtime перегрузок уже нет.

Порядок перегрузок

TypeScript проверяет overload-сигнатуры сверху вниз. Поэтому сначала пишите более специфичные варианты, а потом более общие.

function getSetting(key: "theme"): "light" | "dark";
function getSetting(key: string): string;
function getSetting(key: string): string {
  return localStorage.getItem(key) ?? "";
}

const theme = getSetting("theme"); // "light" | "dark"

Если поставить общий вариант key: string выше, точный результат для "theme" может потеряться. На практике это бьет по автодополнению, условиям в UI и проверкам, которые зависят от конкретного режима.

Практический вывод для frontend-кода

Во frontend overload часто полезен для функций-оберток над API, парсеров, helpers для роутинга и функций с несколькими режимами. Например, один режим возвращает одиночную сущность, другой список.

type User = { id: string; name: string };

function selectUsers(users: User[], mode: "one"): User | undefined;
function selectUsers(users: User[], mode: "many"): User[];
function selectUsers(users: User[], mode: "one" | "many") {
  if (mode === "one") {
    return users[0];
  }

  return users;
}

const firstUser = selectUsers(users, "one");
const allUsers = selectUsers(users, "many");

Такой API помогает компоненту не угадывать тип результата. Для режима "one" UI сразу видит, что результата может не быть, и может показать empty state. Для режима "many" компонент получает массив и не пишет лишние проверки. Но если режимов становится много, лучше подумать об объекте настроек или разделить функцию. Длинный список перегрузок может быть признаком слишком сложного helper-а.

Как построить сильный ответ

Надежный ответ
  1. 1Назвать несколько публичных сигнатур
  2. 2Сказать, что реализация одна
  3. 3Пояснить связь аргументов и результата
  4. 4Упомянуть порядок и проверку в теле функции
Опасный ответ
  1. 1Сказать, что TypeScript создает несколько функций
  2. 2Поставить <code>any</code> и не проверять типы
  3. 3Не объяснить отличие от union
  4. 4Забыть, что широкая перегрузка может скрыть точную

Сильный ответ не обязан быть длинным. Достаточно объяснить контракт, привести маленький пример и сказать, где перегрузки уместны. Если вы добавите отличие от union и упомянете, что реализация одна, ответ будет выглядеть уверенно.

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

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

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

  1. 1

    Путать перегрузку с несколькими реализациями

    В TypeScript у функции остается одна реализация. Если на интервью сказать, что для каждой сигнатуры создается отдельное тело, это будет звучать как перенос опыта из Java или C#. Лучше формулировать так: перегрузки описывают внешний контракт, а внутри вы сами обрабатываете все варианты.
  2. 2

    Использовать overload вместо простого union

    Если функция всегда возвращает один и тот же тип, перегрузки могут только раздуть код. Например, для форматирования string | number в строку часто достаточно одной сигнатуры. Добавляйте перегрузки, когда они дают вызывающему коду более точный тип результата.
  3. 3

    Делать реализацию слишком слабой через any

    any в реализации отключает полезные проверки внутри тела функции. Так легко случайно вызвать метод не того типа и получить runtime-баг. Во frontend это может уронить компонент после ответа API или показать неправильный empty state. Лучше использовать unknown, union или аккуратные проверки typeof, если это возможно.
  4. 4

    Ставить широкую перегрузку выше точной

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

    Забывать про runtime-проверки

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

Follow-up

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

Короткие ответы на вопросы, которыми проверяют понимание перегрузок, union types и сигнатуры реализации.

Живые ответы

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

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

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

Содержание