-
Notifications
You must be signed in to change notification settings - Fork 12.4k
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
Parameter type interface for overloaded functions as union type #32164
Comments
Inferring parameters from a union of function types (which is how overloads are represented internally iirc) typically creates intersections instead of a unions, so this wouldn't do what you want. Case in point: type UnionToIntersection<U> =
(U extends any ? (k: U) => void : never) extends ((k: infer I) => void) ? I : never; |
@fatcerberus overloads are represented as intersections (not unions) of function types... so, by the Power of Contravariance, could be interpreted as operating on unions of function parameters, as requested here. That is, an overloaded function like This issue is a duplicate of (or strongly related to) #14107. Hmm, I just noticed that the "suggestion" template doesn't actually mention how to search for existing issues, nor does it have a "related issues" section like the "bug" template does. Maybe that can be changed? |
Yeah, unions become intersections and vice versa under contravariance, that I knew. I just thought I remembered reading somewhere that overloads were represented internally as unions... huh. Intersections do make more sense though. Thanks for the correction! |
Thank you for responses! Edit: come to think of it, it is basically another aspect of the same issue, since if that worked, this would work by default, since if an overloaded function accepted an union type, then the inference from Parameters<> would also point to the intersection. |
It's not really possible to make this in a way that's both useful and sound. Consider a function like declare function fn(s1: string, s2: string): void;
declare function fn(n1: number, n2: number): void;
declare function doCall<T extends (a1: any, a2: any) => void>(func: T, a0: Parameters<T>[0], a1: Parameters<T>[1]): void; If The current behavior at least makes it so that some calls are correctly accepted. |
The case here though was |
Does the "Design Limitation" label still apply given the previous comment about getting the entire parameter list? This would be helpful for writings tests for an overloaded function where a bunch of inputs and expected outputs are written and checked. For example, I'd like to be able to do this: const tests: Array<{
arguments: Parameters<typeof functionWithOverloads>
expectedOutput
}>= [
{
arguments: [...],
expectedOutput: 1,
},
...
]
tests.forEach(test => assert.deepStrictEqual(
functionWithOverloads(test.arguments),
test.expectedOutput,
)) |
I'm facing this issue as well. My use case is similar to the OP. Regarding @RyanCavanaugh's comment:
I don't see how this is incorrect based on the given declaration since function doCall<T extends (a1: any, a2: any) => void>(func: T, a0: Parameters<T>[0], a1: Parameters<T>[1]): void {
func(a0, a1);
}; Here, because of the Is there something I'm not getting about how the type system works that makes this impossible to implement? |
Any updates with this? Is there any current workaround for achieving the events example provided by the OC? |
Correct me if I'm wrong, but I think the case that @RyanCavanaugh pointed out, can be solved by writing the doCall function using tuples likes this: declare function fn(s1: string, s2: string): void;
declare function fn(n1: number, n2: number): void;
declare function doCall<T extends (a1: any, a2: any) => void>(func: T, ...[a0, a1]: Parameters<T>): void; With this, if we assume A simple playground attempt is here: Playground Link |
I'm trying to set up a list of potential event handlers as tuples, with code and handler, which should then be filtered: |
I wanted to use this, because typing manually all types accepted by Buffer.from is tedious and error-prone - it can break from even patch-to-patch version of // for some reason `Parameters<typeof Buffer.from>[0]` does not work well, resolving to just `string`
type UploadData = Parameters<typeof Buffer.from>[0]; |
It's not perfect but you can tease some info out of the compiler about overloads... up to an arbitrary fixed number of overloads, modulo some weird bugs with zero-arg function types (#28867) Click to expandtype Overloads<T> =
T extends {
(...args: infer A1): infer R1; (...args: infer A2): infer R2;
(...args: infer A3): infer R3; (...args: infer A4): infer R4
} ? [
(...args: A1) => R1, (...args: A2) => R2,
(...args: A3) => R3, (...args: A4) => R4
] : T extends {
(...args: infer A1): infer R1; (...args: infer A2): infer R2;
(...args: infer A3): infer R3
} ? [
(...args: A1) => R1, (...args: A2) => R2,
(...args: A3) => R3
] : T extends {
(...args: infer A1): infer R1; (...args: infer A2): infer R2
} ? [
(...args: A1) => R1, (...args: A2) => R2
] : T extends {
(...args: infer A1): infer R1
} ? [
(...args: A1) => R1
] : any
type OverloadedParameters<T> =
Overloads<T> extends infer O ?
{ [K in keyof O]: Parameters<Extract<O[K], (...args: any) => any>> } : never
type OverloadedReturnType<T> =
Overloads<T> extends infer O ?
{ [K in keyof O]: ReturnType<Extract<O[K], (...args: any) => any>> } : never interface Emitter {
emit(event: 'event_1'): void;
emit(event: 'event_2'): void;
emit(event: 'event_3'): void;
emit(event: 'event_4'): void;
}
type EmitterEmitParams = OverloadedParameters<Emitter["emit"]>
// type EmitterEmitParams = [[event: "event_1"], [event: "event_2"], [event: "event_3"], [event: "event_4"]]
type EventName = OverloadedParameters<Emitter["emit"]>[number][0]
// type EventName = "event_1" | "event_2" | "event_3" | "event_4"
const a: EventName = "event_4";
const b: EventName = "event_1"; |
I have modified jcalz's helpful workaround to make it return unions of tuples like what my own situation calls for (and along the way I arbitrarily added five-argument and six-argument cases): C l i c k t o s e e s o m e c o d etype Overloads<T extends (...args: any[]) => any> =
T extends { (...args: infer A1): infer R1; (...args: infer A2): infer R2; (...args: infer A3): infer R3; (...args: infer A4): infer R4; (...args: infer A5): infer R5; (...args: infer A6): infer R6 } ?
((...args: A1) => R1) | ((...args: A2) => R2) | ((...args: A3) => R3) | ((...args: A4) => R4) | ((...args: A5) => R5) | ((...args: A6) => R6)
: T extends { (...args: infer A1): infer R1; (...args: infer A2): infer R2; (...args: infer A3): infer R3; (...args: infer A4): infer R4; (...args: infer A5): infer R5 } ?
((...args: A1) => R1) | ((...args: A2) => R2) | ((...args: A3) => R3) | ((...args: A4) => R4) | ((...args: A5) => R5)
: T extends { (...args: infer A1): infer R1; (...args: infer A2): infer R2; (...args: infer A3): infer R3; (...args: infer A4): infer R4 } ?
((...args: A1) => R1) | ((...args: A2) => R2) | ((...args: A3) => R3) | ((...args: A4) => R4)
: T extends { (...args: infer A1): infer R1; (...args: infer A2): infer R2; (...args: infer A3): infer R3 } ?
((...args: A1) => R1) | ((...args: A2) => R2) | ((...args: A3) => R3)
: T extends { (...args: infer A1): infer R1; (...args: infer A2): infer R2 } ?
((...args: A1) => R1) | ((...args: A2) => R2)
: T extends { (...args: infer A1): infer R1 } ?
(...args: A1) => R1
: never
type OverloadedParameters<T extends (...args: any[]) => any> = Parameters<Overloads<T>>;
type OverloadedReturnType<T extends (...args: any[]) => any> = ReturnType<Overloads<T>>;
class D
{
go(x: number, y: string): boolean;
go(x: string, y: boolean): number;
go(x: number | string, y: string | boolean): boolean | number
{
if (typeof x === "number")
return x + (y as string).length > 3;
else
return y ? x.length : 4444;
}
stop(a: D, b: boolean): number;
stop(c: number): number;
stop(d: string, e: number, f: D[]): number;
stop(g: string): number;
stop(h: number[], i: number): boolean;
stop(): number;
stop(p0?: unknown, p1?: unknown, p2?: unknown): number | boolean
{
return 3;
}
}
type P = OverloadedParameters<D["go"]>;
let p0: P = [1, "yellow"];
let p1: P = ["two", false];
// @ts-expect-error
let pX: P = [1, true];
type R = OverloadedReturnType<D["go"]>;
let r0: R = 3;
let r1: R = true;
// @ts-expect-error
let rX: R = "no no bad";
type P2 = OverloadedParameters<D["stop"]>;
//type P2 = [a: D, b: boolean] | [c: number] | [d: string, e: number, f: D[]] | [g: string] | [h: number[], i: number] | [];
type R2 = OverloadedReturnType<D["stop"]>;
//type R2 = number | boolean; |
the previous workarounds didn't work for me with the latest version of typescript (or maybe it was the ts opts i had) here was I came up with type FN = (...args: unknown[]) => unknown;
// current typescript version infers 'unknown[]' for any additional overloads
// we can filter them out to get the correct result
type _Params<T> = T extends {
(...args: infer A1): unknown;
(...args: infer A2): unknown;
(...args: infer A3): unknown;
(...args: infer A4): unknown;
(...args: infer A5): unknown;
(...args: infer A6): unknown;
(...args: infer A7): unknown;
(...args: infer A8): unknown;
(...args: infer A9): unknown;
}
? [A1, A2, A3, A4, A5, A6, A7, A8, A9]
: never;
// type T1 = filterUnknowns<[unknown[], string[]]>; // [string[]]
type filterUnknowns<T> = T extends [infer A, ...infer Rest]
? unknown[] extends A
? filterUnknowns<Rest>
: [A, ...filterUnknowns<Rest>]
: T;
// type T1 = TupleArrayUnion<[[], [string], [string, number]]>; // [] | [string] | [string, number]
type TupleArrayUnion<A extends readonly unknown[][]> = A extends (infer T)[]
? T extends unknown[]
? T
: []
: [];
type OverloadParameters<T extends FN> = TupleArrayUnion<filterUnknowns<_Params<T>>>;
declare function fn(): void;
declare function fn(x: 1): void;
declare function fn(s: string, x: 2): void;
type T1 = OverloadParameters<typeof fn>; // [] | [x: 1] | [s: string, x: 2] |
So, I figured I could try to make a recursive version of type OverloadsRecursive<T, U extends any[] = []> =
T extends { (...args: infer A): infer R } & infer O ?
OverloadsRecursive<O, [...U, (...args: A) => R]>
:
never; But This looks like a separate problem, but if it worked it should, I think, allow building the tuple with all overloads up to the recursion limit, which is a lot bigger than any real overload list size. |
…cianBuzzo * rsync: Add typings for "build" static method This change adds typings for the [build static method](https://www.npmjs.com/package/rsync#static-methods). The build method takes an options object, where the keys are Rsync methods and the values are arguments to the methods. I was able to accurately type this using `keyof` and `Parameters`, but to make this work efffectively I had to refactor the overloads in the Rsync interface to use union types instead. This is because overloading and `Paramaters` do not play well together, see microsoft/TypeScript#32164 Change-type: minor Signed-off-by: Lucian Buzzo <lucian.buzzo@gmail.com> * [review] unify `set` signature Co-authored-by: Piotr Błażejewicz <peterblazejewicz@users.noreply.github.com>
@tom-sherman I believe the issue is that using I think this makes sense, as it seems valid to require a function argument to match For example, you might have a function like this:
In these situations, you wouldn't want to allow passing in a function like The fix for this, fortunately, is simple: changing I've updated my post above to reflect this, in case anyone else is using that for reference. |
@paullessing |
No, you're not missing anything - I missed that. 🤦 |
This has some implications with function assignability, as this should not compile because the types of the parameters are not assignable in all of the overloads to the new type declare function overloaded(something: number): void;
declare function overloaded(): void;
type NotOverloaded = (notSomething: boolean) => void;
const notOverloaded: NotOverloaded = overloaded;
notOverloaded(true); |
@eloytoro That's unrelated to overloads, the following also isn't a type error: declare function overloaded(): void;
type NotOverloaded = (notSomething: boolean) => void;
const notOverloaded: NotOverloaded = overloaded;
notOverloaded(true); |
@tom-sherman your example typechecks as it should, because in it the type of The problem here is that not all overloads are checked to check assignability on the type, leading to the faulty code passing typecheck |
It seems that everyone has ignored that the return type of overloaded functions is coupled with their parameter types. |
Cross-linking #29732 |
@madcapnmckay, did you ever figure out how to make this work with generic functions? I updated @Shakeskeyboarde's playground with a breaking example |
Another way of breaking @Shakeskeyboarde solution is using type predicates: type X = OverloadUnion<(value: string | null) => value is string>;
// Type instantiation is excessively deep and possibly infinite.(2589) |
@RyanCavanaugh could you please link to PR which fixes this issue? Thanks! |
Agreed, latest nightly does not pass the example in the OP: export interface Emitter {
emit(event: 'event_1'): void;
emit(event: 'event_2'): void;
emit(event: 'event_3'): void;
emit(event: 'event_4'): void;
}
type EventName = Parameters<Emitter["emit"]>[0]
// ^? type EventName = "event_4" |
The bulk "Close" action I applied to Design Limitation issues doesn't let you pick what kind of "Close" you're doing, so don't read too much into any issue's particular bit on that. I wish GitHub didn't make this distinction without a way to specify it in many UI actions! The correct state for Design Limitation issues is closed since we don't have any plausible way of fixing them, and "Open" issues are for representing "work left to be done". |
Well that's disappointing, but thanks for clarification. |
@RyanCavanaugh I may have missed it but I don't think this comment was ever addressed: #32164 (comment) I'm still curious - what's the design limitation here? |
Search Terms
parameter type interface for overloaded functions as union type
Suggestion
The following method:
Could return an union type when used with overloaded methods instead of the last overload
Use Cases
Message event name safety defined by overloaded signatures
Workaround is to use an enum instead if working in a typescript context.
Examples
Checklist
My suggestion meets these guidelines:
The text was updated successfully, but these errors were encountered: