TypeScript icon indicating copy to clipboard operation
TypeScript copied to clipboard

Parameter type interface for overloaded functions as union type

Open rsxdalv opened this issue 5 years ago • 28 comments

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:

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

rsxdalv avatar Jun 28 '19 09:06 rsxdalv

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;

fatcerberus avatar Jun 28 '19 15:06 fatcerberus

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

jcalz avatar Jun 28 '19 15:06 jcalz

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!

fatcerberus avatar Jun 28 '19 15:06 fatcerberus

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.

rsxdalv avatar Jun 28 '19 15:06 rsxdalv

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.

RyanCavanaugh avatar Jun 28 '19 17:06 RyanCavanaugh

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.

fatcerberus avatar Jun 28 '19 17:06 fatcerberus

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

Tyler-Murphy avatar Jul 24 '19 17:07 Tyler-Murphy

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?

eritbh avatar Oct 23 '19 05:10 eritbh

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

abdatta avatar Aug 09 '20 17:08 abdatta

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

abdatta avatar Aug 10 '20 13:08 abdatta

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.

tvedtorama avatar Oct 02 '20 07:10 tvedtorama

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

falkenhawk avatar Jan 21 '21 09:01 falkenhawk

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

jcalz avatar Jan 21 '21 13:01 jcalz

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;

mjwach avatar Apr 01 '21 03:04 mjwach

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]

derekrjones avatar Aug 02 '21 08:08 derekrjones

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.

biro456 avatar Sep 16 '21 20:09 biro456

I managed to simplify @derekrjones's approach a little so it only needs two types:

type ValidFunction<Arguments extends unknown[], ReturnType> = unknown[] extends Arguments
    ? unknown extends ReturnType 
        ? never 
        : ((...args: Arguments) => ReturnType) 
    : ((...args: Arguments) => ReturnType);

type Overloads<T extends (...args: unknown[]) => unknown> = 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: infer A7): infer R7;
  (...args: infer A8): infer R8;
  (...args: infer A9): infer R9;
  (...args: infer A10): infer R10;
  (...args: infer A11): infer R11;
  (...args: infer A12): infer R12;
}
  ? 
    ValidFunction<A1, R1> |
    ValidFunction<A2, R2> |
    ValidFunction<A3, R3> |
    ValidFunction<A4, R4> |
    ValidFunction<A5, R5> |
    ValidFunction<A6, R6> |
    ValidFunction<A7, R7> |
    ValidFunction<A8, R8> |
    ValidFunction<A9, R9> |
    ValidFunction<A10, R10> |
    ValidFunction<A11, R11> |
    ValidFunction<A12, R12>
  : never;

This collapses any overloads with type (...args: unknown[]) => unknown, and you only end up with overloads that have non-unknown types. If you want to keep those, simply replace ValidFunction with (...args: Arguments) => ReturnType.

Using this approach, we can then also use @mjwach's solution for getting just the return types or argument types:

type OverloadedParameters<T extends (...args: any[]) => unknown> = Parameters<Overloads<T>>;
type OverloadedReturnType<T extends (...args: any[]) => unknown> = ReturnType<Overloads<T>>;

Edit 2022-09-06: Changed the requirement for args in Overloaded{Parameters,ReturnType} from unknown[] to any[] as it was introducing unintended strictness on the arguments

paullessing avatar Mar 18 '22 15:03 paullessing

I figured out a recursive way of converting a function overload (function signature intersection) into a union of the individual signatures: Playground link

type OverloadProps<TOverload> = Pick<TOverload, keyof TOverload>;

type OverloadUnionRecursive<TOverload, TPartialOverload = unknown> = TOverload extends (
  ...args: infer TArgs
) => infer TReturn
  ? // Prevent infinite recursion by stopping recursion when TPartialOverload
    // has accumulated all of the TOverload signatures.
    TPartialOverload extends TOverload
    ? never
    :
        | OverloadUnionRecursive<
            TPartialOverload & TOverload,
            TPartialOverload & ((...args: TArgs) => TReturn) & OverloadProps<TOverload>
          >
        | ((...args: TArgs) => TReturn)
  : never;

type OverloadUnion<TOverload extends (...args: any[]) => any> = Exclude<
  OverloadUnionRecursive<
    // The "() => never" signature must be hoisted to the "front" of the
    // intersection, for two reasons: a) because recursion stops when it is
    // encountered, and b) it seems to prevent the collapse of subsequent
    // "compatible" signatures (eg. "() => void" into "(a?: 1) => void"),
    // which gives a direct conversion to a union.
    (() => never) & TOverload
  >,
  TOverload extends () => never ? never : () => never
>;

// Inferring a union of parameter tuples or return types is now possible.
type OverloadParameters<T extends (...args: any[]) => any> = Parameters<OverloadUnion<T>>;
type OverloadReturnType<T extends (...args: any[]) => any> = ReturnType<OverloadUnion<T>>;

It also appears to work all the way back to TS version 4.1.5. There are a couple of edge cases outlined in the playground code, but they aren't deal breakers for me.

Shakeskeyboarde avatar Jun 05 '22 04:06 Shakeskeyboarde

@Shakeskeyboarde - this works for me as long as none of the overloads are generic functions.

madcapnmckay avatar Jul 01 '22 05:07 madcapnmckay

I'm running into a problem with @derekrjones and @paullessing workarounds when the first overload has one or more params:

declare function fn(x: number): void;
declare function fn(x: 1): void;
type T1 = OverloadParameters<typeof fn>;
Type '{ (x: number): void; (x: 1): void; }' does not satisfy the constraint 'FN'.
  Types of parameters 'x' and 'args' are incompatible.
    Type 'unknown' is not assignable to type 'number'.

Playground

I don't understand why this is happening to be honest, seems like a typescript bug maybe?

tom-sherman avatar Sep 02 '22 15:09 tom-sherman

Also I want to call out a real problem this is causing in adding TypeScript definitions to a popular library: https://github.com/sindresorhus/pify#why-is-pify-choosing-the-last-function-overload-when-using-it-with-typescript

Ideally pify(overloadedFn) would transform each overload with the same transformation as pify(nonOverloadedFn) however due to this current limitations it outputs only the last overload.

I'm trying (and failing right now, see my message above) to integrate the workarounds in this thread but of course this will only help up to a maximum number of overloads, it's really not ideal.

tom-sherman avatar Sep 03 '22 08:09 tom-sherman

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

paullessing avatar Sep 06 '22 18:09 paullessing

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

tom-sherman avatar Sep 06 '22 20:09 tom-sherman

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

paullessing avatar Sep 06 '22 21:09 paullessing

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

eloytoro avatar Sep 27 '22 15:09 eloytoro

@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

tom-sherman avatar Sep 27 '22 16:09 tom-sherman

@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

eloytoro avatar Sep 27 '22 17:09 eloytoro

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

abitwhy avatar Nov 04 '22 08:11 abitwhy

Cross-linking #29732

jcalz avatar Apr 14 '23 14:04 jcalz

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

steveluscher avatar Oct 22 '23 05:10 steveluscher