Hack Frontend Community

Function Overloads в TypeScript

Что такое Function Overloads?

Function Overloads (Перегрузка функций) — это возможность объявить несколько сигнатур для одной функции. TypeScript выберет правильную сигнатуру на основе переданных аргументов.


Базовый синтаксис

// Сигнатуры перегрузок
function greet(name: string): string;
function greet(firstName: string, lastName: string): string;

// Реализация (implementation signature)
function greet(firstName: string, lastName?: string): string {
  if (lastName) {
    return `Hello, ${firstName} ${lastName}!`;
  }
  return `Hello, ${firstName}!`;
}

// Использование
greet('John');              // "Hello, John!"
greet('John', 'Doe');       // "Hello, John Doe!"
// greet('John', 'Doe', 'Jr.');  // Ошибка: Expected 1-2 arguments

Зачем нужны перегрузки?

Разные типы возвращаемого значения

function getValue(key: 'name'): string;
function getValue(key: 'age'): number;
function getValue(key: 'active'): boolean;
function getValue(key: string): string | number | boolean {
  const data = {
    name: 'John',
    age: 30,
    active: true
  };
  return data[key as keyof typeof data];
}

const name = getValue('name');      // string
const age = getValue('age');        // number
const active = getValue('active');  // boolean

Связанные типы аргументов и результата

function createElement(tag: 'div'): HTMLDivElement;
function createElement(tag: 'span'): HTMLSpanElement;
function createElement(tag: 'button'): HTMLButtonElement;
function createElement(tag: string): HTMLElement {
  return document.createElement(tag);
}

const div = createElement('div');      // HTMLDivElement
const span = createElement('span');    // HTMLSpanElement
const button = createElement('button'); // HTMLButtonElement

Опциональные параметры с разными типами

function makeDate(timestamp: number): Date;
function makeDate(year: number, month: number, day: number): Date;
function makeDate(yearOrTimestamp: number, month?: number, day?: number): Date {
  if (month !== undefined && day !== undefined) {
    return new Date(yearOrTimestamp, month, day);
  }
  return new Date(yearOrTimestamp);
}

const date1 = makeDate(1234567890);     // Date
const date2 = makeDate(2024, 11, 19);   // Date
// const date3 = makeDate(2024, 11);    // Ошибка

Порядок сигнатур имеет значение

TypeScript проверяет сигнатуры сверху вниз и использует первую подходящую.

// Неправильно - общая сигнатура первая
function process(value: any): any;
function process(value: string): string;
function process(value: number): number;
function process(value: any): any {
  return value;
}

const result = process('hello');  // any (не string!)

// Правильно - от конкретных к общим
function process(value: string): string;
function process(value: number): number;
function process(value: any): any;
function process(value: any): any {
  return value;
}

const result = process('hello');  // string

Implementation Signature

Реализация должна быть совместима со ВСЕМИ сигнатурами перегрузок.

// Сигнатуры перегрузок
function combine(a: string, b: string): string;
function combine(a: number, b: number): number;

// Реализация должна охватывать оба случая
function combine(a: string | number, b: string | number): string | number {
  if (typeof a === 'string' && typeof b === 'string') {
    return a + b;
  }
  if (typeof a === 'number' && typeof b === 'number') {
    return a + b;
  }
  throw new Error('Invalid arguments');
}

const str = combine('Hello, ', 'World');  // string
const num = combine(10, 20);              // number

Важно:

Implementation signature НЕ видна снаружи. Пользователи видят только overload signatures.


Перегрузки vs Union Types

Когда использовать перегрузки

// С перегрузками - тип зависит от аргумента
function parse(data: string): object;
function parse(data: object): string;
function parse(data: string | object): string | object {
  if (typeof data === 'string') {
    return JSON.parse(data);
  }
  return JSON.stringify(data);
}

const obj = parse('{"name":"John"}');  // object
const str = parse({ name: 'John' });   // string

Когда Union Types достаточно

// Union type проще и понятнее
function format(value: string | number): string {
  return value.toString();
}

Практические примеры

Извлечение из массива

function getFirst<T>(arr: T[]): T | undefined;
function getFirst<T>(arr: T[], count: number): T[];
function getFirst<T>(arr: T[], count?: number): T | T[] | undefined {
  if (count === undefined) {
    return arr[0];
  }
  return arr.slice(0, count);
}

const numbers = [1, 2, 3, 4, 5];
const first = getFirst(numbers);       // number | undefined
const firstThree = getFirst(numbers, 3); // number[]

Поиск элементов

function find(predicate: (item: string) => boolean): string | undefined;
function find(predicate: (item: string) => boolean, all: true): string[];
function find(
  predicate: (item: string) => boolean,
  all?: boolean
): string | string[] | undefined {
  const items = ['apple', 'banana', 'cherry'];
  
  if (all) {
    return items.filter(predicate);
  }
  return items.find(predicate);
}

const one = find(item => item.startsWith('a'));      // string | undefined
const many = find(item => item.startsWith('a'), true); // string[]

Event Listener

function addEventListener(
  element: HTMLElement,
  event: 'click',
  handler: (e: MouseEvent) => void
): void;

function addEventListener(
  element: HTMLElement,
  event: 'keypress',
  handler: (e: KeyboardEvent) => void
): void;

function addEventListener(
  element: HTMLElement,
  event: string,
  handler: (e: Event) => void
): void {
  element.addEventListener(event, handler);
}

const button = document.querySelector('button')!;

addEventListener(button, 'click', (e) => {
  // e: MouseEvent
  console.log(e.clientX);
});

addEventListener(button, 'keypress', (e) => {
  // e: KeyboardEvent
  console.log(e.key);
});

API Wrapper

function request(method: 'GET', url: string): Promise<Response>;
function request(
  method: 'POST' | 'PUT',
  url: string,
  body: object
): Promise<Response>;

function request(
  method: string,
  url: string,
  body?: object
): Promise<Response> {
  const options: RequestInit = { method };
  
  if (body) {
    options.body = JSON.stringify(body);
    options.headers = { 'Content-Type': 'application/json' };
  }
  
  return fetch(url, options);
}

// Использование
request('GET', '/api/users');
request('POST', '/api/users', { name: 'John' });
// request('POST', '/api/users');  // Ошибка: body обязателен

Перегрузки методов класса

class DataStore {
  private data: Record<string, any> = {};
  
  // Перегрузки методов
  get(key: 'name'): string;
  get(key: 'age'): number;
  get(key: 'active'): boolean;
  get(key: string): any {
    return this.data[key];
  }
  
  set(key: 'name', value: string): void;
  set(key: 'age', value: number): void;
  set(key: 'active', value: boolean): void;
  set(key: string, value: any): void {
    this.data[key] = value;
  }
}

const store = new DataStore();
store.set('name', 'John');    // OK
store.set('age', 30);         // OK
// store.set('age', 'thirty'); // Ошибка

const name = store.get('name');  // string
const age = store.get('age');    // number

Перегрузки конструкторов

class Point {
  x: number;
  y: number;
  
  constructor(x: number, y: number);
  constructor(coords: { x: number; y: number });
  constructor(xOrCoords: number | { x: number; y: number }, y?: number) {
    if (typeof xOrCoords === 'number') {
      this.x = xOrCoords;
      this.y = y!;
    } else {
      this.x = xOrCoords.x;
      this.y = xOrCoords.y;
    }
  }
}

const p1 = new Point(10, 20);
const p2 = new Point({ x: 10, y: 20 });

Generics в перегрузках

function map<T, U>(arr: T[], fn: (item: T) => U): U[];
function map<T, U>(arr: T[], fn: (item: T, index: number) => U): U[];
function map<T, U>(
  arr: T[],
  fn: (item: T, index?: number) => U
): U[] {
  return arr.map(fn as any);
}

const numbers = [1, 2, 3];
const doubled = map(numbers, n => n * 2);
const indexed = map(numbers, (n, i) => `${i}: ${n}`);

Типичные ошибки

Несовместимая реализация

// Ошибка: реализация не совместима с перегрузками
function process(value: string): string;
function process(value: number): number;
function process(value: boolean): boolean {  // Ошибка!
  return value;
}

Забыть про порядок

// Плохо - общий случай перекрывает конкретные
function handle(value: any): any;
function handle(value: string): string;  // Недостижимо!
function handle(value: any): any {
  return value;
}

Слишком много перегрузок

// Плохо - слишком сложно
function format(value: string): string;
function format(value: number): string;
function format(value: boolean): string;
function format(value: Date): string;
function format(value: object): string;
// ... еще 10 перегрузок

// Лучше использовать union или generic
function format(value: string | number | boolean | Date | object): string {
  return String(value);
}

Best Practices

Используйте перегрузки только когда необходимо

// Не нужны перегрузки
function add(a: number, b: number): number {
  return a + b;
}

// Нужны перегрузки - разные типы результата
function getValue(key: 'name'): string;
function getValue(key: 'age'): number;
function getValue(key: string): any {
  // ...
}

Держите перегрузки близко к реализации

// Хорошо - все вместе
function process(value: string): string;
function process(value: number): number;
function process(value: string | number): string | number {
  return value;
}

Документируйте сложные перегрузки

/**
 * Creates a date from timestamp
 * @param timestamp - Unix timestamp in milliseconds
 */
function makeDate(timestamp: number): Date;

/**
 * Creates a date from year, month, and day
 * @param year - Full year (e.g., 2024)
 * @param month - Month (0-11)
 * @param day - Day of month (1-31)
 */
function makeDate(year: number, month: number, day: number): Date;

function makeDate(yearOrTimestamp: number, month?: number, day?: number): Date {
  // ...
}

Альтернативы перегрузкам

Conditional Types

type ReturnType<T> = T extends 'string' ? string :
                     T extends 'number' ? number :
                     T extends 'boolean' ? boolean :
                     never;

function getValue<T extends 'string' | 'number' | 'boolean'>(
  type: T
): ReturnType<T> {
  // ...
  return null as any;
}

Discriminated Unions

type Options = 
  | { type: 'GET'; url: string }
  | { type: 'POST'; url: string; body: object };

function request(options: Options): Promise<Response> {
  // ...
}

Вывод

Function Overloads:

  • Позволяют объявить несколько сигнатур для одной функции
  • Выбор сигнатуры зависит от аргументов
  • Порядок сигнатур важен (от конкретных к общим)
  • Implementation signature должна быть совместима со всеми перегрузками
  • Полезны когда тип результата зависит от аргументов
  • Альтернативы: union types, conditional types, discriminated unions
  • Не злоупотребляйте — используйте только когда необходимо

На собеседовании:

Важно уметь:

  • Объяснить синтаксис перегрузки функций
  • Написать пример с разными типами результата
  • Объяснить разницу между overload и implementation signatures
  • Показать важность порядка сигнатур
  • Рассказать когда перегрузки лучше union types
  • Назвать альтернативы (conditional types, discriminated unions)