← Курс/Перегрузка функций (Function Overloads)#153 из 257+25 XP

Перегрузка функций (Function Overloads)

Что такое перегрузка функций

В TypeScript **function overloads** позволяют объявить несколько сигнатур для одной функции. Это нужно когда функция ведёт себя по-разному в зависимости от типов аргументов.

// Перегрузка: разные сигнатуры вызова
function createDate(timestamp: number): Date
function createDate(month: number, day: number, year: number): Date

// Реализация (не видна снаружи):
function createDate(monthOrTimestamp: number, day?: number, year?: number): Date {
  if (day !== undefined && year !== undefined) {
    return new Date(year, monthOrTimestamp - 1, day)
  } else {
    return new Date(monthOrTimestamp)
  }
}

// Вызовы:
createDate(1609459200000)   // OK — по timestamp
createDate(1, 15, 2024)     // OK — месяц, день, год
createDate(15, 2024)        // Ошибка TS: нет подходящей перегрузки

Структура function overloads

// 1. Сигнатуры перегрузок (overload signatures) — только для TypeScript, не выполняются:
function reverse(str: string): string
function reverse(arr: number[]): number[]

// 2. Сигнатура реализации — должна быть совместима со всеми перегрузками:
function reverse(input: string | number[]): string | number[] {
  if (typeof input === 'string') {
    return input.split('').reverse().join('')
  }
  return [...input].reverse()
}

// TypeScript знает точные типы при вызове:
const str = reverse('hello')    // тип: string
const arr = reverse([1, 2, 3])  // тип: number[]

Правила перегрузки

1. Должно быть **минимум 2 сигнатуры перегрузки**

2. Сигнатура реализации **не участвует** в разрешении перегрузки — она только реализует логику

3. Сигнатура реализации должна быть **совместима** со всеми перегрузками

4. Более **специфичные** сигнатуры должны идти **раньше** общих

// Плохо — менее специфичная сигнатура первой может затенить другие:
function format(value: string | number): string     // слишком общая
function format(value: number, digits: number): string // никогда не выбирается!

// Хорошо — специфичные первыми:
function format(value: number, digits: number): string
function format(value: string | number): string

Практический пример: getElementById

Классический пример из DOM API:

// В TypeScript встроено (упрощённо):
function querySelector(selector: string): Element | null
function querySelector<T extends Element>(selector: string): T | null

// Реальный usecase:
const input = document.querySelector<HTMLInputElement>('#name')
// input: HTMLInputElement | null

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

Используй перегрузки когда:

  • Функция принимает **разные типы** и **возвращает разные типы** в зависимости от входных данных
  • Нужно документировать несколько способов вызова
  • Не используй когда:

  • Достаточно union типов: string | number
  • Достаточно optional параметров: function foo(a: string, b?: number)
  • Достаточно generic: function identity<T>(x: T): T
  • // Не нужна перегрузка — достаточно optional параметра:
    function greet(name: string, greeting?: string): string {
      return `${greeting ?? 'Привет'}, ${name}!`
    }
    
    // Нужна перегрузка — разные входные/выходные типы:
    function parse(input: string): string[]
    function parse(input: string[]): string
    function parse(input: string | string[]): string | string[] {
      if (typeof input === 'string') return input.split(',')
      return input.join(',')
    }

    Примеры

    Паттерны перегрузки функций в JavaScript: dispatch по типу аргументов

    // В TS: function overloads — разные сигнатуры для одной функции
    // В JS: реализуем через проверку типов аргументов
    
    // === Паттерн 1: reverse для строк и массивов ===
    function reverse(input) {
      // В TS:
      // function reverse(str: string): string
      // function reverse(arr: number[]): number[]
      if (typeof input === 'string') {
        return input.split('').reverse().join('')
      }
      if (Array.isArray(input)) {
        return [...input].reverse()
      }
      throw new TypeError(`Ожидается строка или массив, получено: ${typeof input}`)
    }
    
    console.log('=== reverse ===')
    console.log(reverse('hello'))       // 'olleh'
    console.log(reverse('TypeScript'))  // 'tpircSepyT'
    console.log(reverse([1, 2, 3, 4]))  // [4, 3, 2, 1]
    console.log(reverse(['a', 'b', 'c'])) // ['c', 'b', 'a']
    
    // === Паттерн 2: createDate — по timestamp или по дате ===
    function createDate(...args) {
      // В TS:
      // function createDate(timestamp: number): Date
      // function createDate(month: number, day: number, year: number): Date
      if (args.length === 1 && typeof args[0] === 'number') {
        return new Date(args[0])
      }
      if (args.length === 3) {
        const [month, day, year] = args
        return new Date(year, month - 1, day)
      }
      throw new Error(`Неверные аргументы: ожидается (timestamp) или (month, day, year)`)
    }
    
    console.log('\n=== createDate ===')
    console.log(createDate(0))            // Thu Jan 01 1970...
    console.log(createDate(1, 15, 2024))  // Mon Jan 15 2024...
    
    try {
      createDate(1, 15)  // В TS: ошибка — нет подходящей перегрузки
    } catch (e) {
      console.log(e.message)
    }
    
    // === Паттерн 3: parse строки и массива ===
    function parse(input) {
      // В TS:
      // function parse(input: string): string[]
      // function parse(input: string[]): string
      if (typeof input === 'string') {
        return input.split(',').map(s => s.trim())
      }
      if (Array.isArray(input)) {
        return input.join(', ')
      }
      throw new TypeError(`Ожидается строка или массив строк`)
    }
    
    console.log('\n=== parse ===')
    console.log(parse('red, green, blue'))  // ['red', 'green', 'blue']
    console.log(parse(['one', 'two', 'three']))  // 'one, two, three'
    
    // === Паттерн 4: format числа и даты ===
    function format(value, ...rest) {
      // В TS:
      // function format(value: number, digits: number): string
      // function format(value: Date, locale: string): string
      if (typeof value === 'number') {
        const digits = rest[0] ?? 2
        return value.toFixed(digits)
      }
      if (value instanceof Date) {
        const locale = rest[0] ?? 'ru-RU'
        return value.toLocaleDateString(locale)
      }
      throw new TypeError(`Ожидается число или дата`)
    }
    
    console.log('\n=== format ===')
    console.log(format(3.14159, 2))         // '3.14'
    console.log(format(1000, 0))            // '1000'
    console.log(format(new Date('2024-01-15'), 'ru-RU'))  // '15.01.2024'