Skip to content
Open
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,8 +64,9 @@ export type {MultidimensionalReadonlyArray} from './source/multidimensional-read
export type {IterableElement} from './source/iterable-element.d.ts';
export type {Entry} from './source/entry.d.ts';
export type {Entries} from './source/entries.d.ts';
export type {SetReturnType} from './source/set-return-type.d.ts';
export type {FunctionOverloads} from './source/function-overloads.d.ts';
export type {SetParameterType} from './source/set-parameter-type.d.ts';
export type {SetReturnType} from './source/set-return-type.d.ts';
export type {Asyncify} from './source/asyncify.d.ts';
export type {Simplify} from './source/simplify.d.ts';
export type {SimplifyDeep} from './source/simplify-deep.d.ts';
Expand Down
8 changes: 6 additions & 2 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -145,8 +145,6 @@ Click the type names for complete docs.
- [`IterableElement`](source/iterable-element.d.ts) - Get the element type of an `Iterable`/`AsyncIterable`. For example, `Array`, `Set`, `Map`, generator, stream, etc.
- [`Entry`](source/entry.d.ts) - Create a type that represents the type of an entry of a collection.
- [`Entries`](source/entries.d.ts) - Create a type that represents the type of the entries of a collection.
- [`SetReturnType`](source/set-return-type.d.ts) - Create a function type with a return type of your choice and the same parameters as the given function type.
- [`SetParameterType`](source/set-parameter-type.d.ts) - Create a function that replaces some parameters with the given parameters.
- [`Simplify`](source/simplify.d.ts) - Useful to flatten the type output to improve type hints shown in editors. And also to transform an interface into a type to aide with assignability.
- [`SimplifyDeep`](source/simplify-deep.d.ts) - Deeply simplifies an object type.
- [`Get`](source/get.d.ts) - Get a deeply-nested property from an object using a key path, like [Lodash's `.get()`](https://lodash.com/docs/latest#get) function.
Expand Down Expand Up @@ -233,6 +231,12 @@ Click the type names for complete docs.
- [`AsyncReturnType`](source/async-return-type.d.ts) - Unwrap the return type of a function that returns a `Promise`.
- [`Asyncify`](source/asyncify.d.ts) - Create an async version of the given function type.

### Function

- [`FunctionOverloads`](source/function-overloads.d.ts) - Create a union of all the function's overloads.
- [`SetParameterType`](source/set-parameter-type.d.ts) - Create a function that replaces some parameters with the given parameters.
- [`SetReturnType`](source/set-return-type.d.ts) - Create a function type with a return type of your choice and the same parameters as the given function type.

### String

- [`Trim`](source/trim.d.ts) - Remove leading and trailing spaces from a string.
Expand Down
96 changes: 96 additions & 0 deletions source/function-overloads.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import type {FunctionWithMaybeThisParameter} from './internal/function.d.ts';
import type {IsEqual} from './is-equal.d.ts';

/**
Create a union of all the function's overloads.
TypeScript's built-in utility types like `Parameters` and `ReturnType` only work with the last overload signature, [by design](https://github.com/microsoft/TypeScript/issues/32164). This type extracts all overload signatures as a union, allowing you to work with each overload individually.
Use-cases:
- Extract parameter types from specific overloads using `Extract` and `Parameters`
- Analyze all possible function signatures in type-level code
- Extract event handler signatures from framework APIs
Known limitions:
- Functions that have identical parameters but different `this` types or return types will only extract one overload (the last one)
- Generic type parameters are lost and inferred as `unknown`
Comment on lines +15 to +17
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add at least one example for each case.

@example
```ts
import type {FunctionOverloads} from 'type-fest';
function request(url: string): Promise<string>;
function request(url: string, options: {json: true}): Promise<unknown>;
function request(url: string, options?: {json?: boolean}) {
// ...
}
type RequestFunctionType = FunctionOverloads<typeof request>;
//=> ((url: string) => Promise<string>) | ((url: string, options: {json: true}) => Promise<unknown>)
// You can also get all parameters and return types using built-in `Parameters` and `ReturnType` utilities:
type RequestParameters = Parameters<RequestFunctionType>;
//=> [url: string, options: {json: true}] | [url: string]
type RequestReturnType = ReturnType<RequestFunctionType>;
//=> Promise<string> | Promise<unknown>
```
This type can also be used to extract event parameter types from framework emit functions:
@example
```ts
// Given a Vue component that defines its events:
defineEmits<{
submit: [formData: FormData];
cancel: [];
}>();
// Extract the parameter types of the `submit` event:
import type {ArrayTail, FunctionOverloads} from 'type-fest';
import HelloWorld from './HelloWorld.vue';
type SubmitEventType = ArrayTail<Parameters<Extract<FunctionOverloads<InstanceType<typeof HelloWorld>['$emit']>, (event: 'submit', ...arguments_: readonly any[]) => void>>>;
//=> [formData: FormData]
```
Comment on lines +44 to +57
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Examples should not contain arbitrary code, they should be copy pasteable to the playground.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you mean the TypeScript playground?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, like the import HelloWorld from './HelloWorld.vue'; import statement would error if pasted to playground.

@category Function
*/
export type FunctionOverloads<T> = FunctionOverloadsInternal<T>;

type FunctionOverloadsInternal<
AllOverloads,
CheckedOverloads = {},
MustStopIfParametersAreEqual extends boolean = true,
LastParameters = never,
> = AllOverloads extends (
this: infer ThisType,
...arguments_: infer ParametersType extends readonly unknown[]
) => infer ReturnType
? // This simultaneously checks if the last and the current parameters are equal and `MustStopIfParametersAreEqual` flag is true
IsEqual<
[LastParameters, true],
[
ParametersType,
[MustStopIfParametersAreEqual] extends [true] ? true : false, // Prevents distributivity
]
> extends true
? never
:
| FunctionOverloadsInternal<
// Normally, in `(FunctionType extends (...args: infer P) => infer R)`, compiler infers
// `P` and `R` from the last function overload.
// This trick (intersecting one of the function signatures with the full signature)
// makes compiler infer a last overload that do not equal one of the concatenated ones.
// Thus, we're ending up iterating over all the overloads from bottom to top.
// Credits: https://github.com/microsoft/TypeScript/issues/32164#issuecomment-1146737709
CheckedOverloads & AllOverloads,
CheckedOverloads & ((this: ThisType, ...arguments_: ParametersType) => ReturnType),
MustStopIfParametersAreEqual extends true ? false : true,
ParametersType
>
| FunctionWithMaybeThisParameter<ThisType, ParametersType, ReturnType>
: never;
Comment on lines 63 to 95
Copy link
Contributor

@benzaria benzaria Oct 11, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why only checking the Parameters and not This and ReturnType as well.
this code below is working fine and passed all tests!

Suggested change
type FunctionOverloadsInternal<
AllOverloads,
CheckedOverloads = {},
MustStopIfParametersAreEqual extends boolean = true,
LastParameters = never,
> = AllOverloads extends (
this: infer ThisType,
...arguments_: infer ParametersType extends readonly unknown[]
) => infer ReturnType
? // This simultaneously checks if the last and the current parameters are equal and `MustStopIfParametersAreEqual` flag is true
IsEqual<
[LastParameters, true],
[
ParametersType,
[MustStopIfParametersAreEqual] extends [true] ? true : false, // Prevents distributivity
]
> extends true
? never
:
| FunctionOverloadsInternal<
// Normally, in `(FunctionType extends (...args: infer P) => infer R)`, compiler infers
// `P` and `R` from the last function overload.
// This trick (intersecting one of the function signatures with the full signature)
// makes compiler infer a last overload that do not equal one of the concatenated ones.
// Thus, we're ending up iterating over all the overloads from bottom to top.
// Credits: https://github.com/microsoft/TypeScript/issues/32164#issuecomment-1146737709
CheckedOverloads & AllOverloads,
CheckedOverloads & ((this: ThisType, ...arguments_: ParametersType) => ReturnType),
MustStopIfParametersAreEqual extends true ? false : true,
ParametersType
>
| FunctionWithMaybeThisParameter<ThisType, ParametersType, ReturnType>
: never;
type FunctionOverloadsInternal<
AllOverloads,
CheckedOverloads = {},
MustStopIfOverloadsAreEqual extends boolean = true,
LastOverload extends UnknownArray = [],
> = AllOverloads extends (
this: infer ThisType,
...arguments_: infer ParametersType extends UnknownArray
) => infer ReturnType
? // This simultaneously checks if the last and the current parameters are equal and `MustStopIfParametersAreEqual` flag is true
IsEqual<
[...LastOverload, MustStopIfOverloadsAreEqual],
[ThisType, ParametersType, ReturnType, true]
> extends true
? never
:
| FunctionOverloadsInternal<
// Normally, in `(FunctionType extends (...args: infer P) => infer R)`, compiler infers
// `P` and `R` from the last function overload.
// This trick (intersecting one of the function signatures with the full signature)
// makes compiler infer a last overload that do not equal one of the concatenated ones.
// Thus, we're ending up iterating over all the overloads from bottom to top.
// Credits: https://github.com/microsoft/TypeScript/issues/32164#issuecomment-1146737709
CheckedOverloads & AllOverloads,
CheckedOverloads & FunctionWithMaybeThisParameter<ThisType, ParametersType, ReturnType>,
Not<MustStopIfOverloadsAreEqual>,
[ThisType, ParametersType, ReturnType]
>
| FunctionWithMaybeThisParameter<ThisType, ParametersType, ReturnType>
: never;

this will eliminate the current limitation of this and returnType overloads.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When parameters are the same up to this or return type, TypeScript would use only one overload. So the fact that FunctionOverloads would only return a single overload actually matches the "real" compiler behavior.

With this types being different, it chooses the last overload:

declare const foo: ((this: string) => void) & ((this: number) => string);
foo.call(''); // ts(2345): Argument of type 'string' is not assignable to parameter of type 'number'.

With return types being different, it chooses the first overload for some reason:

declare const foo: (() => string) & (() => number);
const bar = foo(); // string

And in this case, FunctionOverloads lies about the actual return type:

type Test = FunctionOverloads<(() => string) & (() => number)>; // () => number

So this might need to be fixed, but currently this also matches the built-in ReturnType behavior:

type Test = ReturnType<(() => string) & (() => number)>; // number

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I understand your concern about how overload resolution works in TypeScript’s compile-time enforcement, but it’s important to clarify: the exception behavior you point out applies to overload selection during type checking, not to the type-level utility we’re building.

TypeScript will resolve function calls to the first matching overload signature, ignoring differences in return type and sometimes in this context. However, this only affects how function calls are checked, not how overloads are declared or represented at the type level.

declare const foo: ((this: string) => void) & ((this: number) => string);
type T = typeof foo
//   ^? type T = ((this: string) => void) & ((this: number) => string)
// overloads still preserved

Our type utility is meant for static analysis and broader use cases, not just for expressing runtime call behavior. Users might want to use FunctionOverloads<T> to:

  • Inspect all declared overloads for documentation or introspection
  • Metaprogramming and type-level transformations
  • Build stricter API transformations or mappings

These use cases go beyond how the runtime implementation behaves. So while matching TypeScript’s call resolution is a good baseline, we shouldn’t constrain the type utility only to what the type checker “chooses” for calls.

In short: the overload resolution rules are a valid observation, but they don’t limit what our type utilities can represent. If anything, the distinction shows the need for a richer type-level union so developers can see all overload signatures, even those that TypeScript’s call resolution might ignore during checking.

@sindresorhus , @som-sm could you share your thoughts on this?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I understand your point too, but IMHO matching the actual compiler behavior is more practical and will be sufficient in 95% of situations. If some of the declared overloads get discarded by the compiler, it's likely the sign of the programmer not knowing about this TypeScript peculiarity.

If anything, we can introduce a type utility option to control whether these "phantom" overloads will be present in the resulting type or not, but I'm not sure what practical example would I use in this option's docs.


export {};
18 changes: 18 additions & 0 deletions source/internal/function.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import type {IsUnknown} from '../is-unknown.d.ts';

/**
Constructs a function type with an optional `this` parameter.
If `this` parameter is not needed, pass `unknown`.
*/
export type FunctionWithMaybeThisParameter<
ThisParameter,
Parameters_ extends readonly unknown[],
TypeToReturn,
> =
// If a function does not specify the `this` parameter, it will be inferred to `unknown`
IsUnknown<ThisParameter> extends true
? (...args: Parameters_) => TypeToReturn
: (this: ThisParameter, ...args: Parameters_) => TypeToReturn;

export {};
1 change: 1 addition & 0 deletions source/internal/index.d.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
export type * from './array.d.ts';
export type * from './characters.d.ts';
export type * from './function.d.ts';
export type * from './keys.d.ts';
export type * from './numeric.d.ts';
export type * from './object.d.ts';
Expand Down
20 changes: 10 additions & 10 deletions source/set-parameter-type.d.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import type {IsUnknown} from './is-unknown.d.ts';
import type {StaticPartOfArray, VariablePartOfArray} from './internal/index.d.ts';
import type {FunctionWithMaybeThisParameter, StaticPartOfArray, VariablePartOfArray} from './internal/index.d.ts';
import type {UnknownArray} from './unknown-array.d.ts';

/**
Expand Down Expand Up @@ -104,16 +103,17 @@ type HandleLog2 = SetParameterType<HandleMessage, {2: string}>;

@category Function
*/
export type SetParameterType<Function_ extends (...arguments_: any[]) => unknown, P extends Record<number, unknown>> =
export type SetParameterType<
Function_ extends (...arguments_: any[]) => unknown,
P extends Record<number, unknown>,
> =
// Just using `Parameters<Fn>` isn't ideal because it doesn't handle the `this` fake parameter.
Function_ extends (this: infer ThisArgument, ...arguments_: infer Arguments) => unknown
? (
// If a function did not specify the `this` fake parameter, it will be inferred to `unknown`.
// We want to detect this situation just to display a friendlier type upon hovering on an IntelliSense-powered IDE.
IsUnknown<ThisArgument> extends true
? (...arguments_: MergeObjectToArray<Arguments, P>) => ReturnType<Function_>
: (this: ThisArgument, ...arguments_: MergeObjectToArray<Arguments, P>) => ReturnType<Function_>
)
? FunctionWithMaybeThisParameter<
ThisArgument,
MergeObjectToArray<Arguments, P>,
ReturnType<Function_>
>
: Function_; // This part should be unreachable

export {};
14 changes: 5 additions & 9 deletions source/set-return-type.d.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type {IsUnknown} from './is-unknown.d.ts';
import type {FunctionWithMaybeThisParameter} from './internal/index.d.ts';

/**
Create a function type with a return type of your choice and the same parameters as the given function type.
Expand All @@ -19,13 +19,9 @@ type MyWrappedFunction = SetReturnType<MyFunctionThatCanThrow, SomeOtherType | u
*/
export type SetReturnType<Function_ extends (...arguments_: any[]) => any, TypeToReturn> =
// Just using `Parameters<Fn>` isn't ideal because it doesn't handle the `this` fake parameter.
Function_ extends (this: infer ThisArgument, ...arguments_: infer Arguments) => any ? (
// If a function did not specify the `this` fake parameter, it will be inferred to `unknown`.
// We want to detect this situation just to display a friendlier type upon hovering on an IntelliSense-powered IDE.
IsUnknown<ThisArgument> extends true ? (...arguments_: Arguments) => TypeToReturn : (this: ThisArgument, ...arguments_: Arguments) => TypeToReturn
) : (
// This part should be unreachable, but we make it meaningful just in case…
(...arguments_: Parameters<Function_>) => TypeToReturn
);
Function_ extends (this: infer ThisArgument, ...arguments_: infer Arguments) => any
? FunctionWithMaybeThisParameter<ThisArgument, Arguments, TypeToReturn>
: // This part should be unreachable, but we make it meaningful just in case…
(...arguments_: Parameters<Function_>) => TypeToReturn;

export {};
109 changes: 109 additions & 0 deletions test-d/function-overloads.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
import {expectType} from 'tsd';
import type {FunctionOverloads} from '../source/function-overloads.d.ts';

type Function1 = (foo: string, bar: number) => object;
type Function2 = (foo: bigint, ...bar: any[]) => void;

declare const normalFunction: FunctionOverloads<Function1>;
expectType<Function1>(normalFunction);

// Note: function overload is equivalent to intersecting its signatures

declare const twoOverloads: FunctionOverloads<Function1 & Function2>;
expectType<Function1 | Function2>(twoOverloads);

declare const twoIdenticalOverloads: FunctionOverloads<Function1>;
expectType<Function1>(twoIdenticalOverloads);

type Function3 = (foo: string, bar: number, baz?: boolean) => object;

declare const twoOverloadsWithAssignableSignature: FunctionOverloads<Function1 & Function3>;
expectType<Function1 | Function3>(twoOverloadsWithAssignableSignature);

declare const threeOverloads: FunctionOverloads<Function1 & Function2 & Function3>;
expectType<Function1 | Function2 | Function3>(threeOverloads);

type Function4 = (...foo: any[]) => void;
type Function5 = (...foo: readonly any[]) => void;

declare const normalFunctionWithOnlyRestWritableParameter: FunctionOverloads<Function4>;
expectType<Function4>(normalFunctionWithOnlyRestWritableParameter);

declare const normalFunctionWithOnlyRestReadonlyParameter: FunctionOverloads<Function5>;
expectType<Function5>(normalFunctionWithOnlyRestReadonlyParameter);

declare const twoOverloadsWithDifferentRestParameterReadonliness: FunctionOverloads<
Function4 & Function5
>;
// Expected: it seems like the compiler ignores subsequent identical up to `readonly` modifier overloads
expectType<Function4>(twoOverloadsWithDifferentRestParameterReadonliness);

declare const twoOverloadsWithDifferentRestParameterReadonlinessReversed: FunctionOverloads<
Function5 & Function4
>;
// Expected: it seems like the compiler ignores subsequent identical up to `readonly` modifier overloads
expectType<Function5>(twoOverloadsWithDifferentRestParameterReadonlinessReversed);

type Function6 = (foo: string, ...bar: any[]) => void;
type Function7 = (foo: string, ...bar: readonly any[]) => void;

declare const normalFunctionWithNormalAndRestWritableParameter: FunctionOverloads<Function6>;
expectType<Function6>(normalFunctionWithNormalAndRestWritableParameter);

declare const normalFunctionWithNormalAndRestReadonlyParameter: FunctionOverloads<Function7>;
// Expected: readonly rest parameter cannot be represented with tuples
expectType<(foo: string, ...bar: any[]) => void>(normalFunctionWithNormalAndRestReadonlyParameter);

type Function8 = () => never;

declare const normalFunctionNoParameters: FunctionOverloads<Function8>;
expectType<Function8>(normalFunctionNoParameters);

declare const twoOverloadsWithNoAndPresentParameters: FunctionOverloads<Function8 & Function6>;
expectType<Function8 | Function6>(twoOverloadsWithNoAndPresentParameters);

type Function9 = (event: 'event9', arg: string) => void;
type Function10 = (event: 'event10', arg: number) => string;
type Function11 = (event: 'event11', arg: boolean) => never;
type Function12 = (event: 'event12', arg: bigint) => object;

declare const manyOverloads: FunctionOverloads<
Function1 &
Function2 &
Function3 &
Function4 &
Function5 &
Function6 &
Function7 &
Function8 &
Function9 &
Function10 &
Function11 &
Function12
>;
expectType<
| Function1
| Function2
| Function3
| Function4
| Function5
| Function6
| Function7
| Function8
| Function9
| Function10
| Function11
| Function12
>(manyOverloads);

declare const noOverloads: FunctionOverloads<{}>;
expectType<never>(noOverloads);

declare const anyOverload: FunctionOverloads<any>;
expectType<(...arguments_: readonly unknown[]) => unknown>(anyOverload);

declare const neverOverload: FunctionOverloads<never>;
expectType<never>(neverOverload);

declare const unknownOverload: FunctionOverloads<unknown>;
expectType<never>(unknownOverload);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

some test for This and ReturnType overloads

Suggested change
expectType<never>(unknownOverload);
expectType<never>(unknownOverload);
type ThisFunction1 = (this: {a: 1}, foo: string) => void;
type ThisFunction2 = (this: {b: 2}, foo: string) => void;
declare const thisOverloads: FunctionOverloads<ThisFunction1 & ThisFunction2>;
expectType<ThisFunction1 | ThisFunction2>(thisOverloads);
type ReturnFunction1 = (foo: string) => string;
type ReturnFunction2 = (foo: string) => number;
declare const returnOverloads: FunctionOverloads<ReturnFunction1 & ReturnFunction2>;
expectType<ReturnFunction1 | ReturnFunction2>(returnOverloads);