open atlas
↑ К треку
Система типов TypeScript вглубь TS · 06 · 01

Перегрузки функций

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

TS Middle ◷ 14 min
Уровень
ОсновыJuniorMiddleSenior
Уже знаешь этот юнит? Пройди быструю проверку за минуту →

Коллега выкатывает хелпер createElement(tag). Для createElement("a") он хочет HTMLAnchorElement; для createElement("canvas")HTMLCanvasElement; для любой неизвестной строки — HTMLElement. Одна сигнатура, возвращающая HTMLElement, теряет точность по тегу, а возврат-объединение заставляет каждого вызывающего заново сужать тип. Перегрузки — это инструмент, который позволяет одной функции выставить сразу несколько точных контрактов, но только если вы точно понимаете, как компилятор выбирает, какой контракт достанется конкретному вызову.

Форма: много сигнатур, одно тело

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

// Сигнатуры перегрузки (публичный контракт)
function len(x: string): number;
function len(x: unknown[]): number;
// Сигнатура реализации (внутренняя — НЕ вызываемая перегрузка)
function len(x: string | unknown[]): number {
  return x.length;
}

len("hello");
//  ^? number
len([1, 2, 3]);
//  ^? number
len(123);
// Error: No overload matches this call.
//   Argument of type 'number' is not assignable to parameter of type 'string'.

Третий вызов падает, хотя параметр реализации — string | unknown[], а 123 не подходит ни под одну сигнатуру вызова. Ключевое правило: вызывающие могут вызывать только сигнатуры перегрузки. Сигнатура реализации расширяет взгляд тела, но не входит в публичный тип.

Порядок разрешения: сверху вниз, побеждает первое совпадение

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

Когда вы пишете вызов, компилятор идёт по списку перегрузок с верха и связывает вызов с первой сигнатурой, чьи параметры принимают аргументы. Он не ищет «лучшее» совпадение — берёт первое подходящее. Поэтому порядок важен: ставьте более специфичные сигнатуры выше более общих.

function pick(x: "a"): 1;
function pick(x: string): 2;
function pick(x: string): 1 | 2 {
  return x === "a" ? 1 : 2;
}

pick("a");
//   ^? 1   — первая сигнатура совпала, поиск остановлен
pick("b");
//   ^? 2   — первая не совпала с литералом, вторая совпадает

Поменяйте две сигнатуры местами — и pick("a") разрешится в 2, потому что string (теперь первый) уже принимает "a", и поиск никогда не доходит до литеральной перегрузки.

Сигнатура реализации должна покрывать каждую перегрузку

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

function f(x: string): string;
function f(x: number): number;
function f(x: string): string {
  //       ^^^^^^^^^ Error: This overload signature is not compatible
  //                 with its implementation signature.
  return x;
}

Здесь перегрузка x: number обещает принимать число, но реализация принимает только string. Фикс — расширить параметр реализации до string | number (и обычно возврат до string | number). Сигнатура реализации не проверяется против вызывающих — она может быть свободнее, — но проверяется против перегрузок, которые она поддерживает.

Почему это работает

Почему сигнатура реализации не вызывается? Потому что она существует, чтобы типизировать тело, а не интерфейс. Будь она вызываемой, перегруженный len(x: string | unknown[]) пропускал бы len(maybeStringOrArray) даже тогда, когда вы хотели запретить объединение в точке вызова. Удержание реализации приватной позволяет публичным сигнатурам быть строго уже того, что технически терпит тело — и в этом весь смысл перегрузок.

Когда перегрузки выигрывают у возврата-объединения, а когда проигрывают

Перегрузки оправдывают себя, когда тип возврата зависит от конкретного типа аргумента так, как простое объединение выразить не может. createElement("a") → HTMLAnchorElement против createElement("canvas") → HTMLCanvasElement — канонический случай: одна сигнатура с возвратом HTMLElement заставила бы каждого вызывающего делать каст.

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

// Перегрузки: три сигнатуры, никакой связи, переиспользуемой компилятором
function wrapO(x: string): string[];
function wrapO(x: number): number[];
function wrapO(x: string | number): (string | number)[] {
  return [x];
}

// Один дженерик: связывает вход и выход один раз, масштабируется на любой T
function wrap<T>(x: T): T[] {
  return [x];
}
wrap("hi");
//   ^? string[]
wrap(42);
//   ^? number[]

const u: string | number = Math.random() > 0.5 ? "x" : 1;
wrapO(u);
// Error: No overload matches this call — аргумент-объединение не подходит ни одной перегрузке.
wrap(u);
//   ^? (string | number)[]   — дженерик справляется с объединением даром

Беритесь за перегрузки только когда отображение вход/выход дискретное и нерегулярное (разные литеральные входы → несвязанные типы выхода). Когда отображение однородное, предпочтите дженерик; когда оно по правилу над типом, предпочтите условный тип возврата. Оба композируются лучше и остаются надёжными.

Викторина

Вы пишете сигнатуры перегрузки `(x: string): 'wide'`, затем `(x: 'id'): 'narrow'` (в таком порядке) с совместимой реализацией. Во что разрешится `f('id')` и почему?

Расставь шаги по порядку

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

  1. 1 Собрать сигнатуры перегрузки (вызова) в порядке исходника, игнорируя сигнатуру реализации
  2. 2 Начать с первой сигнатуры перегрузки
  3. 3 Проверить, присваиваемы ли аргументы вызова параметрам этой сигнатуры
  4. 4 На первой совпавшей сигнатуре связать вызов и остановить поиск
  5. 5 Если ни одна перегрузка не совпала, сообщить 'No overload matches this call'
Вспомните перед уходом
  1. 01
    Проследите, как именно компилятор разрешает вызов перегруженной функции, и почему важен порядок перегрузок.
  2. 02
    Когда стоит браться за перегрузки, а когда дженерик или условный тип возврата — лучший инструмент?
Итог

Перегрузки функций выставляют несколько публичных сигнатур вызова над одной приватной сигнатурой реализации. Компилятор разрешает вызов, проходя сигнатуры перегрузки сверху вниз и связывая с первой, принимающей аргументы, — первое совпадение, а не лучшее, — поэтому специфичные сигнатуры должны идти перед общими. Сигнатура реализации должна быть супертипом каждой перегрузки и никогда не вызывается снаружи функции. Используйте перегрузки только для дискретных нерегулярных отображений вход→выход; иначе берите дженерик или условный тип возврата. Дальше мы оставим параметры и посмотрим на скрытый первый параметр, который тайно есть у каждого метода: this, как TypeScript типизирует его фейковым параметром this: T и как он теряется, когда метод передают как колбэк. Теперь, когда вы видите, что перегрузка возвращает неожиданный тип для литерального аргумента, первым делом проверяйте порядок сигнатур — специфичная перегрузка могла оказаться под общей, которая поглощает её первой.

Практика

Начни сверху. Задачи идут от простого к сложному: вспомнить факт, применить к случаю, затем senior-уровень. Открой, попробуй, потом открой ответ.

вспомнитьприменитьуглубить0 из 7 завершено
Связанные уроки

Что-то непонятно?

Задай вопрос по этому уроку. Вопросы анонимны и попадают напрямую автору — урок станет лучше.

хоткеи развернуть
поиск
K
пред. пьеса
k
след. пьеса
j
тиры
t
это меню
?
sources1
expand
  1. 01

Trademarks belong to their respective owners. Editorial reference only.