Skip to content
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

Closed
5 tasks done
rsxdalv opened this issue Jun 28, 2019 · 36 comments
Closed
5 tasks done

Parameter type interface for overloaded functions as union type #32164

rsxdalv opened this issue Jun 28, 2019 · 36 comments
Labels
Design Limitation Constraints of the existing architecture prevent this from being fixed

Comments

@rsxdalv
Copy link

rsxdalv commented Jun 28, 2019

Search Terms

parameter type interface for overloaded functions as union type

Suggestion

The following method:

/**
 * Obtain the parameters of a function type in a tuple
 */
type Parameters<T extends (...args: any[]) => any> = T extends (...args: infer P) => any ? P : never;

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

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]
// is -> type EventName = "event_4"
// wanted -> type EventName = "event_1" | "event_2" | "event_3" | "event_4"
const a: EventName = "event_4";
const b: EventName = "event_1";
// error, because -> const b: "event_4"

Checklist

My suggestion meets these guidelines:

  • This wouldn't be a breaking change in existing TypeScript/JavaScript code
  • This wouldn't change the runtime behavior of existing JavaScript code
  • This could be implemented without emitting different JS based on the types of the expressions
  • This isn't a runtime feature (e.g. library functionality, non-ECMAScript syntax with JavaScript output, etc.)
  • This feature would agree with the rest of TypeScript's Design Goals.
@fatcerberus
Copy link

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: UnionToIntersection:

type UnionToIntersection<U> = 
  (U extends any ? (k: U) => void : never) extends ((k: infer I) => void) ? I : never;

@jcalz
Copy link
Contributor

jcalz commented Jun 28, 2019

@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 { (x: string): number; (x: number): string } will definitely accept a string parameter and will definitely accept a number parameter. So you should be able to safely widen that to {(x: string | number): string | number}.


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?

@fatcerberus
Copy link

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!

@rsxdalv
Copy link
Author

rsxdalv commented Jun 28, 2019

Thank you for responses!
@jcalz I haven't turned off the github search for issues when submitting, but the keywords I searched didn't pop up, some of the specific issues clash with very popular and broad keywords, making finding them hard. I hadn't found that issue.
That issue is related, and I might've seen it before, and it's related in implementation, though slightly different in end result.

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.

@RyanCavanaugh RyanCavanaugh added the Design Limitation Constraints of the existing architecture prevent this from being fixed label Jun 28, 2019
@RyanCavanaugh
Copy link
Member

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 Parameters<T>[0] returns string | number, then doCall(fn, 0, "") would incorrectly succeed. If Parameters<T>[0]> returns string & number, then doCall(fn, 0, 0) would incorrectly fail (and be a big breaking change). Notably, with conditional types and unions, really the only kind of functions that can't be typed with a single overload are exactly the ones that have this failure mode.

The current behavior at least makes it so that some calls are correctly accepted.

@fatcerberus
Copy link

The case here though was infer over the entire parameter list which in the case above would yield [string,string] | [number,number]—which precisely describes the valid inputs to fn.

@Tyler-Murphy
Copy link

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,
))

@eritbh
Copy link

eritbh commented Oct 23, 2019

I'm facing this issue as well. My use case is similar to the OP. Regarding @RyanCavanaugh's comment:

If Parameters<T>[0] returns string | number, then doCall(fn, 0, "") would incorrectly succeed.

I don't see how this is incorrect based on the given declaration since (func: T, ...args: Parameters<T>) could be used to achieve a more correct typing, and this is how I plan to use it in my use case (which is similar to OP's). However, with an implementation attached, additional type constraints are introduced anyway:

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 func(a0, a1) call, it should ideally be implied that [a0, a1] are compatible with Parameters<typeof func>.

Is there something I'm not getting about how the type system works that makes this impossible to implement?

@abdatta
Copy link

abdatta commented Aug 9, 2020

Any updates with this? Is there any current workaround for achieving the events example provided by the OC?

@abdatta
Copy link

abdatta commented Aug 10, 2020

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 Parameters<fn> gives [string, string] | [number, number], then doCall(fn, 0, "") would not succeed anymore, and only doCall(fn, 0, 1) or doCall(fn, "0", "") will succeed.

A simple playground attempt is here: Playground Link

@tvedtorama
Copy link

I'm trying to set up a list of potential event handlers as tuples, with code and handler, which should then be filtered: on("x", (pX, pT) => {}), on("y", (pZZ) => {}. Parameters<myQueue.on> only gives the tuple for the last handler, not all the possible combinations. I don't have the luxury to alter the type definitions, so a way to extract all the possible tuples in a union type would be nice.

@falkenhawk
Copy link

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 @types/node, like it just did for me.

// for some reason `Parameters<typeof Buffer.from>[0]` does not work well, resolving to just `string`
type UploadData = Parameters<typeof Buffer.from>[0];

@jcalz
Copy link
Contributor

jcalz commented Jan 21, 2021

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 expand
type 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";

Playground link

@mjwach
Copy link

mjwach commented Apr 1, 2021

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 e
type 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;

@derekrjones
Copy link

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
I was only interested in parameters, so no return type here

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]

@biro456
Copy link

biro456 commented Sep 16, 2021

So, I figured I could try to make a recursive version of Overloads<T> in an effort to make it cover virtually any function:

type OverloadsRecursive<T, U extends any[] = []> =
  T extends { (...args: infer A): infer R } & infer O ?
    OverloadsRecursive<O, [...U, (...args: A) => R]>
  :
    never;

But O always ends up being T, instead of an object with the remaining fields/overloads.

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.

typescript-bot pushed a commit to DefinitelyTyped/DefinitelyTyped that referenced this issue Dec 15, 2021
…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>
@paullessing
Copy link

paullessing commented Sep 6, 2022

@tom-sherman I believe the issue is that using unknown in the input arguments is actually acting as a strict requirement, rather than a "I don't know what this will be" kind of requirement. So when the code says type FN = (...args: readonly any[]) => unknown; it's actually requiring args to be unknown[], rather than being loose and saying "I won't know what this is".

I think this makes sense, as it seems valid to require a function argument to match (value: unknown) => void, i.e. "you cannot pass in a function that takes number; it has to treat all inputs as unknown because I will not guarantee any restrictions on it".

For example, you might have a function like this:

function registerErrorHandler(handler: (error: unknown) => void);

In these situations, you wouldn't want to allow passing in a function like (error: string) => void. So using unknown as the narrowest type seems reasonable to me in function arguments.

The fix for this, fortunately, is simple: changing ...args: unknown[] to ...args: any[] resolves the issue, as we would be changing the type from the narrowest to the widest, allowing all types of input.

Working Playground

I've updated my post above to reflect this, in case anyone else is using that for reference.

@tom-sherman
Copy link

@paullessing T1 has the type of never in your playground, am I missing something?

@paullessing
Copy link

No, you're not missing anything - I missed that. 🤦
I'll have another look when I next get time, sorry!

@eloytoro
Copy link

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);

Playground

@tom-sherman
Copy link

@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);

Playground: https://www.typescriptlang.org/play?ts=4.8.2#code/CYUwxgNghgTiAEAzArgOzAFwJYHtXxwDcQYIcpRgAKASgC55CctgBuAKHYwE8AHBAHI4MAeWKlyleAF54VVMIDKOALYgMACyyoA5gwBGOHBBBRUNGQD5GzNpzB4AzhngLR4shRDAGQ9yU8pWSIAyW8OdjcxUK9qDBhkEBpWIA

@eloytoro
Copy link

@tom-sherman your example typechecks as it should, because in it the type of overloaded is a sub-type of NotOverloaded, and if notOverloaded were called with a parameter it would not matter since overloaded ignores parameters passed to it, so it's also safe in the runtime

The problem here is that not all overloads are checked to check assignability on the type, leading to the faulty code passing typecheck

@abitwhy
Copy link

abitwhy commented Nov 4, 2022

It seems that everyone has ignored that the return type of overloaded functions is coupled with their parameter types.

@jcalz
Copy link
Contributor

jcalz commented Apr 14, 2023

Cross-linking #29732

@steveluscher
Copy link

@madcapnmckay, did you ever figure out how to make this work with generic functions? I updated @Shakeskeyboarde's playground with a breaking example TestA4 (link).

@rijenkii
Copy link

rijenkii commented Nov 11, 2023

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)

@adrian-gierakowski
Copy link

@RyanCavanaugh could you please link to PR which fixes this issue? Thanks!

@rijenkii
Copy link

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"

Playground

@RyanCavanaugh
Copy link
Member

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".

@RyanCavanaugh RyanCavanaugh closed this as not planned Won't fix, can't repro, duplicate, stale Feb 25, 2024
@rijenkii
Copy link

Well that's disappointing, but thanks for clarification.

@Tyler-Murphy
Copy link

@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?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Design Limitation Constraints of the existing architecture prevent this from being fixed
Projects
None yet
Development

No branches or pull requests