Typescript: Parameter type interface for overloaded functions as union type

Created on 28 Jun 2019  路  11Comments  路  Source: microsoft/TypeScript

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.
Design Limitation

Most helpful comment

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

All 11 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: 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]鈥攚hich 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,
))

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.

Was this page helpful?
0 / 5 - 0 ratings