microsoft / TypeScript

TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

Home Page:https://www.typescriptlang.org

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Parameter type interface for overloaded functions as union type

rsxdalv opened this issue · 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:

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

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

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

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.

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.

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

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?

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

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.

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

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

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;

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]

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.

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

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 - this works for me as long as none of the overloads are generic functions.

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?

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 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 T1 has the type of never in your playground, am I missing something?

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

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

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 TestA4 (link).

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"

Playground

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?