Функции
Функция в TypeScript — это тело плюс сигнатура, и вся тема сводится к тому, что компилятор делает с сигнатурой и чего он с ней сделать не может. Сигнатура — это не документация: она проверяется на каждой стороне вызова, участвует в выводе типов колбэков (контекстная типизация — параметры стрелки внутри map не аннотируют не от лени, а потому что их тип уже задан целевым типом) и полностью стирается перед выпуском JavaScript. Отсюда следует главный водораздел: всё, что вы объявляете о функции, — это обещание компилятору, а не код, который выполнится.
Из этого стирания выводятся все ловушки темы. Перегрузки — набор видимых вызывающему сигнатур над одной реализацией: никакой диспетчеризации по типам в рантайме не появляется, ветвление вы пишете руками, а сигнатура реализации вообще не вызываема. Параметр this — фиктивный первый параметр, который аргументом не является: он лишь типизирует получателя и исчезает при эмите, а у стрелочной функции его не бывает вовсе, потому что this в ней захвачен лексически. Обобщённая обёртка над функцией сохраняет контракт, только если типизирована по типу самой функции (<F extends (...args: any[]) => any>), а не по Function, — и даже тогда параметр типа обёрнутой обобщённой функции обёртка не переносит. Разбор по слоям — ниже.
Карта темы
- Типы функций — выражение
(x: number) => string, call-сигнатура, контекстная типизация колбэка и поблажка дляvoid. - Параметры функций — необязательный
?, значение по умолчанию=, rest...args, правило порядка и почему?и= значениедают разные типы внутри тела. - Перегрузки функций — несколько видимых сигнатур над одной невызываемой реализацией и когда union-тип честнее.
- Типизация this — фиктивный параметр
this: T,ThisParameterType/OmitThisParameter, флагnoImplicitThisи почему у стрелки параметраthisнет. - Обёртки над функциями — memoize, debounce и логирующая обёртка, сохраняющие сигнатуру через
Parameters<F>/ReturnType<F>, и предел этого приёма на обобщённой функции.
Частые ошибки и ловушки
| Ошибка | Последствие | |
|---|---|---|
Ставить необязательный ?-параметр перед обязательным | Ошибка компиляции — необязательные идут только после обязательных | |
Считать ? и = значение одним и тем же | У ? тип внутри тела — `T | undefined; у параметра по умолчанию — просто T` |
Писать rest как args...: T или ставить его не последним | Синтаксическая ошибка — только ...args: T[] и только последним параметром | |
| Аннотировать параметры колбэка, переданного в типизированную функцию | Их тип уже задан контекстной типизацией — лишняя аннотация только даёт шанс ошибиться | |
Ждать от () => void запрета возвращать значение | Функция, возвращающая что угодно, подходит под () => void — вызывающий просто игнорирует результат | |
| Считать, что перегрузки выбирают тело по типам аргументов в рантайме | Перегрузки — этап компиляции; тело одно, ветвление внутри пишете вы, сигнатуры стираются | |
| Пытаться вызвать функцию по сигнатуре реализации | Вызывающему видны только сигнатуры перегрузок; реализация скрыта, даже если она шире | |
| Брать перегрузки там, где результат не зависит от формы аргумента | Лишняя сложность — union-тип в одном параметре читается яснее | |
Думать, что параметр this нужно передавать аргументом | Это фиктивный параметр — он только типизирует получателя и стирается при эмите | |
Объявлять параметр this у стрелочной функции | Ошибка компиляции — стрелка захватывает this лексически и своего получателя не имеет | |
Ждать, что аннотация this: T привяжет получателя в рантайме | Она ничего не привязывает — привязку по-прежнему делают bind / call / стрелка | |
Типизировать обёртку как (fn: Function) => Function | Контракт обёрнутой функции стёрт: вызовы результата больше не проверяются | |
Возвращать из обёртки (...args: any[]) => any | То же самое другими словами — типы аргументов и результата потеряны | |
| Ждать, что обёртка сохранит параметр типа обобщённой функции | F фиксируется на конкретной инстанциации — обобщённость обёрткой не переносится | |
Забывать про this в обёртке, которой оборачивают метод | Обёртка, вызвавшая fn(...args) вместо fn.apply(this, args), теряет получателя |
Значение для собеседований
Функции спрашивают не ради синтаксиса, а чтобы проверить, отделяете ли вы контракт от реализации и этап компиляции от рантайма. Три вопроса делают это одним ударом: «что происходит с перегрузками после компиляции», «что делает this первым параметром» и «как типизировать debounce, чтобы не потерять параметры». Каждый из них проверяет одно и то же — понимаете ли вы, что сигнатура проверяется, а не исполняется.
Что обычно проверяют:
- Что параметры стоит аннотировать, а результат обычно выводится — и что параметры колбэка типизируются контекстно.
- Порядок параметров и разницу между
?и значением по умолчанию. - Тип функции
(x: T) => R, call-сигнатуру и поблажку присваивания для() => void. - Как устроены перегрузки, почему сигнатура реализации не вызываема и когда union-тип проще.
- Что такое параметр
this, почему он не аргумент, чем помогаетnoImplicitThisи что делаетstrictBindCallApply. - Как типизировать обёртку через
<F extends (...args: any[]) => any>иParameters<F>/ReturnType<F>— и где этот приём упирается в предел с обобщённой функцией.
Типичный неверный ответ: «перегрузки — это когда в рантайме по типам аргументов выбирается нужное тело». Тела всего одно, выбор делаете вы сами внутри него, а все сигнатуры перегрузок исчезают при эмите. Второй классический провал — типизировать debounce как (fn: Function, ms: number) => Function: код скомпилируется, но с этого момента ни один вызов результата не проверяется — обёртка, задуманная как безобидная, стала дырой в типизации размером со всю функцию.