Skip to content

Add Overloads, OverloadParameters, OverloadReturnType#1399

Open
ikeyan wants to merge 27 commits into
sindresorhus:mainfrom
ikeyan:feat/function-overloads-v2
Open

Add Overloads, OverloadParameters, OverloadReturnType#1399
ikeyan wants to merge 27 commits into
sindresorhus:mainfrom
ikeyan:feat/function-overloads-v2

Conversation

@ikeyan

@ikeyan ikeyan commented Apr 8, 2026

Copy link
Copy Markdown

Fixes #868
Fixes #585
Closes #1264

Summary

Adds three types for working with overloaded function signatures:

  • Overloads<F> — extracts all overloads as a tuple, preserving declaration order
  • OverloadParameters<F> — overload-aware Parameters (returns a union of all parameter tuples)
  • OverloadReturnType<F> — overload-aware ReturnType (returns a union of all return types)

TypeScript's built-in Parameters and ReturnType only work with the last overload; these types extract all of them. To get a union of overloads instead of a tuple, use Overloads<F>[number].

Improvements over #1264

  • Renamed from FunctionOverloads to Overloads and changed the return type from a union to a tuple — overloads are ordered, so a tuple is the natural representation. Analogous to Parameters / ReturnType in naming style
  • Added OverloadParameters and OverloadReturnType as direct replacements for Parameters and ReturnType on overloaded functions
  • HasExplicitThis correctly distinguishes implicit this from explicit this: unknown by exploiting TypeScript's overload deduplication rules
  • Fixed a bug in Add FunctionOverloads type #1264 where encountering an overload with the same parameter list as a previous one would terminate iteration early, missing remaining overloads
  • CollectOverloads uses an effect-observation loop with N-advancement to extract overloads preceding generic overloads, which the previous convergence-based approach could not reach
  • Internal documentation explains TypeScript's overload enumeration behavior (deduplication rules, implicit this semantics)

Known limitations

  • Generic overloads:
    • Type parameters are lost -- they are replaced by their upper bound (e.g. <T> becomes unknown, <T extends string> becomes string)
    • When there are 4 or more generic overloads, extraction stops at the 4th-from-last generic overload and any overloads before it are omitted
    • Implicit this (no annotation) is indistinguishable from explicit this: unknown, so the output always includes this: unknown
  • TypeScript deduplicates overloads that it considers identical. When both overloads have explicit this, all three of (This, Parameters, Return) must match to be deduplicated. However, when one or both have implicit this (no annotation), only (Parameters, Return) is compared — meaning an implicit-this overload and an explicit-this overload with the same params/return are treated as duplicates, and whichever appears first suppresses the other

ikeyan and others added 4 commits April 8, 2026 18:17
Create a union of all overload signatures of a given function type.
TypeScript's built-in `Parameters` and `ReturnType` only work with the
last overload — this type extracts all of them.

The implementation correctly distinguishes between implicit `this` and
explicit `this: unknown` overloads via a two-pass approach.

Resolves sindresorhus#868

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Neither tsd's `expectType` nor type-fest's `IsEqual` can distinguish
`() => void` from `(this: unknown) => void`. Add a test-local
`IsEqualStrict` helper that extracts the `this` parameter via a
sentinel type, and use it in tests that verify this distinction.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Move `SetReturnType`, `SetParameterType`, and `FunctionOverloads` from
Utilities into a new Function section, matching their `@category` tags.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@ikeyan

ikeyan commented Apr 9, 2026

Copy link
Copy Markdown
Author

Naming suggestion

I'd like to propose renaming FunctionOverloads before merging. Two concerns with the current name:

  1. The Function prefix is redundant — "overloads" already implies functions
  2. The name doesn't convey that the result is a union of the overload signatures

Suggested alternatives:

  • OverloadsToUnion — follows the existing UnionToIntersection, UnionToTuple, TupleToUnion naming pattern
  • OverloadsUnion — shorter, similar to LiteralUnion, TaggedUnion

I'd lean toward OverloadsToUnion for consistency with the XToY conversion types already in the library. What do you think @sindresorhus?

- CollectOverloads now builds a tuple instead of a union, preserving
  declaration order. Duplicates from the termination lag are removed by
  dropping the first element at return.
- Replace recursive tuple iterations (MatchesAnyOverload,
  ExtractExplicitUnknownThisOverloads, IsImplicitThisOverload,
  OverloadsTupleToUnion) with a single mapped type (OverloadsToFunctions).
- DistinguishUnknownThisOverloads now returns a tuple of function types;
  FunctionOverloads indexes with [number] to produce the union.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Comment thread source/function-overloads.d.ts Outdated
Comment thread source/function-overloads.d.ts Outdated
Comment thread source/overloads.d.ts
ikeyan and others added 2 commits April 10, 2026 19:43
Replace the two-pass approach with a simpler design:

- `HasExplicitThis`: detects implicit vs explicit `this: unknown` by
  intersecting a `(this: Nothing, ...)` sentinel from the right — implicit
  `this` absorbs it (TS deduplication), explicit `this: unknown` doesn't.
- `LastOverload`: extracts the last overload as a standalone function with
  correct `this` handling.
- `CollectOverloads`: uses `LastOverload` directly, no intermediate
  `[This, Params, Return]` tuples or secondary function type.

Also:
- Export `OverloadsToTuple` (tuple counterpart to `FunctionOverloads`)
- Fix `Parameters` extraction for `readonly` arrays
- Add detailed internal documentation of TypeScript's overload enumeration
  behavior (deduplication rules, implicit `this` semantics)
- Update tests to target `OverloadsToTuple` with tuple expectations

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Move `HasExplicitThis`, `LastOverload`, `CollectOverloads`, and
  internal `Parameters` to `source/internal/function.d.ts`
- Deduplicate JSDoc: TS overload enumeration behavior documented once
  in the internal file; `HasExplicitThis` and `CollectOverloads` refer
  to it instead of repeating
- Fix stale "in this file" reference in `FunctionOverloads` JSDoc
- Add "Known limitations are the same as FunctionOverloads" to
  `OverloadsToTuple` JSDoc

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@ikeyan ikeyan changed the title Add FunctionOverloads type Add OverloadsToUnion and OverloadsToTuple types Apr 11, 2026
@ikeyan ikeyan changed the title Add OverloadsToUnion and OverloadsToTuple types Add OverloadsToUnion, OverloadsToTuple, OverloadParameters, OverloadReturnType Apr 11, 2026
… drop `OverloadsToUnion`

- Rename `OverloadsToTuple` → `Overloads` (tuple is the natural representation)
- Remove `OverloadsToUnion` (redundant — `Overloads<F>[number]` is equivalent)
- Add `OverloadParameters` and `OverloadReturnType` as overload-aware
  counterparts to the built-in `Parameters` and `ReturnType`

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@ikeyan ikeyan changed the title Add OverloadsToUnion, OverloadsToTuple, OverloadParameters, OverloadReturnType Add Overloads, OverloadParameters, OverloadReturnType Apr 11, 2026
@sindresorhus

Copy link
Copy Markdown
Owner

Overloads does not just lose the generic parameter, it can drop earlier non-generic overloads entirely. A minimal repro:

declare function genericLast(input: string): 1;
declare function genericLast<Type>(input: Type): Type;

type Result = Overloads<typeof genericLast>;
// actual:   [(this: unknown, input: unknown) => unknown]
// expected: [(input: string) => 1, (input: unknown) => unknown]

Same thing happens when the generic overload is in the middle of the list:

declare function genericMiddle(input: string): 1;
declare function genericMiddle<Type>(input: Type): Type;
declare function genericMiddle(input: number): 2;

type Result = Overloads<typeof genericMiddle>;
// actual:   [(this: unknown, input: unknown) => unknown, (input: number) => 2]
// expected: [(input: string) => 1, (input: unknown) => unknown, (input: number) => 2]

I reran this locally with tsd to make sure I wasn't misreading it. So the current limitation is a bit bigger then the docs say: generic overloads can suppress earlier overloads altogether, which also makes OverloadParameters and OverloadReturnType wrong for those cases.

Comment thread source/internal/function.d.ts Outdated
Comment thread source/internal/function.d.ts Outdated
ikeyan and others added 6 commits April 12, 2026 17:43
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…ncement

Rewrite `CollectOverloads` internals to handle generic overloads that
previously terminated the iteration, losing all preceding overloads.

- Replace `LastOverload` with `NthLastOverload<F, N>`: uses a multi-signature
  pattern (MaxOverloadPatterns=4 slots) to extract the Nth-from-last overload,
  with `HasExplicitThis` generalized to check any position.

- Replace convergence-based loop with effect-observation loop: each iteration
  checks whether intersecting ExtractedN onto AllOverloads changes the view at
  position N. If it does (effect), intersect and continue. If not (no effect,
  e.g. aliasing generic overloads), output without intersecting and advance N.

- Add `() => Unique` sentinel boundary: `CollectOverloads` prepends the
  sentinel before the original overloads so `NthLastOverload` can detect when
  all original overloads have been exhausted.

- Add `FuncToTupleForCompare` for this-aware function comparison, reused by
  test `IsEqualStrict`.

- Update Known limitations: generic type parameters are replaced by their
  upper bound; functions with <4 generic overloads are fully extracted.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Use a default type parameter (`AllOverloads = (() => Unique) & F`) to
prepend the sentinel, eliminating the separate wrapper type. Update doc
to reflect the unified signature.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Comment thread source/internal/function.d.ts Outdated
Comment thread source/internal/function.d.ts Outdated
Comment thread source/internal/function.d.ts Outdated
Comment thread source/internal/function.d.ts Outdated
Comment thread source/internal/function.d.ts
Comment thread source/function-overloads.d.ts Outdated
Comment thread source/function-overloads.d.ts Outdated
Comment thread source/overloads.d.ts
@som-sm

som-sm commented Apr 17, 2026

Copy link
Copy Markdown
Collaborator

Also, looks like the explicit this logic is not quite working with generic functions, refer:

type T = Overloads<{<T>(x: T): T}>;
//=> [(this: unknown, x: unknown) => unknown]

Comment thread source/internal/function.d.ts Outdated
@ikeyan

ikeyan commented Apr 21, 2026

Copy link
Copy Markdown
Author

Also, looks like the explicit this logic is not quite working with generic functions, refer:

type T = Overloads<{<T>(x: T): T}>;
//=> [(this: unknown, x: unknown) => unknown]

Added to known limitations

@ikeyan

ikeyan commented May 26, 2026

Copy link
Copy Markdown
Author

@sindresorhus

Right. Same underlying limitation, just bounded by a larger constant now.

Quick context first: those repros were against the version of the code at the time the review landed, which used a 1-slot NthLastOverload. The code now uses a 4-slot template, so applying those exact examples to current Overloads no longer drops anything: Overloads<typeof genericLast> returns the [(input: string) => 1, (input: unknown) => unknown] you expected. But the structural problem is unchanged; only the constant moved from 1 to 4. To reproduce the same cascade against the current code you need ≥ 4 generic overloads in the source, which is what the existing limitations note calls out.

Why it remains structural: the technique matches the function against a target shape whose slots are non-generic call signatures introduced by infer ((this: infer Ti, ...args: infer Pi): infer Ri). infer cannot bind a type parameter inside a captured signature (only variables bound to the enclosing conditional), so a generic call signature cannot be captured as generic in a slot; every captured slot is monomorphized to the signature's constraint form (so <Type>(input: Type) => Type instantiates to (input: unknown) => unknown). NthLastOverload itself does not halt on a generic overload as long as the source has fewer generic overloads than the slot count, but once the count exceeds the slot count, the monomorphized slot collides with the original generic when intersected back onto AllOverloads in CollectOverloads's advancement step. The dedup-merge there is what causes overloads on the lossy side of the collision to disappear. That is exactly why in the original 1-slot repros the (input: string) => 1 before the generic vanished while the (input: number) => 2 after survived.

So the limitation is bounded by a constant rather than absent. Closing it (rather than just pushing the constant up further) would need TypeScript itself to grow a way to capture a single, possibly-generic call signature through conditional inference, such as extends infer F extends AnyNonOverloadedFunction, where the constraint pins F to one non-overloaded signature so each overload could be matched one at a time. That does not exist today.

@som-sm

som-sm commented Jun 7, 2026

Copy link
Copy Markdown
Collaborator

@ikeyan I've slightly cleaned up overload.d.ts in 6282d78, please review once.

No logic changes, just removed the AllOverloads type argument, and inlined comments.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

A type util to produce an alternation of function overloads. OverloadParameters and OverloadReturnType

4 participants