-
-
Notifications
You must be signed in to change notification settings - Fork 647
Add FunctionOverloads type
#1264
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Add FunctionOverloads type
#1264
Conversation
e312009 to
0087818
Compare
0087818 to
61ef2cc
Compare
|
I think microsoft/TypeScript#14107 may be relevant. Maybe link to it in the doc comment. |
|
Thank you for the review!
I'm not sure how I'd link this issue. I think microsoft/TypeScript#32164 is actually more relevant (and the author of the trick I've used also left the same comment about the method of getting union of overloads), but I don't see additional value of including it the docs too. |
It can be useful as context to the limitation. |
|
Mentioned the issue that this type addresses. Also removed the limitation of losing declare function foo(bar: string, ...baz: readonly number[]): void;
declare const array: number[];
foo('', ...array); // Allowed despite `array` not being readonly |
FunctionOverloads typeFunctionOverloads type
| 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; |
There was a problem hiding this comment.
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!
| 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.
There was a problem hiding this comment.
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(); // stringAnd in this case, FunctionOverloads lies about the actual return type:
type Test = FunctionOverloads<(() => string) & (() => number)>; // () => numberSo this might need to be fixed, but currently this also matches the built-in ReturnType behavior:
type Test = ReturnType<(() => string) & (() => number)>; // numberThere was a problem hiding this comment.
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 preservedOur 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?
There was a problem hiding this comment.
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.
| expectType<never>(neverOverload); | ||
|
|
||
| declare const unknownOverload: FunctionOverloads<unknown>; | ||
| expectType<never>(unknownOverload); |
There was a problem hiding this comment.
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
| 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); |
| 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` |
There was a problem hiding this comment.
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.
| ```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] | ||
| ``` |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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?
Resolves #868 and resolves partially #585