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

proper overloading for arrow functions #47669

Open
5 tasks done
DetachHead opened this issue Jan 30, 2022 · 11 comments
Open
5 tasks done

proper overloading for arrow functions #47669

DetachHead opened this issue Jan 30, 2022 · 11 comments
Labels
Needs Proposal This issue needs a plan that clarifies the finer details of how it could be implemented. Suggestion An idea for TypeScript

Comments

@DetachHead
Copy link
Contributor

Suggestion

πŸ” Search Terms

arrow function overload

βœ… Viability 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, new syntax sugar for JS, etc.)
  • This feature would agree with the rest of TypeScript's Design Goals.

⭐ Suggestion

it seems that the syntax for overloading arrow functions are more like a side effect of existing functionality (function interfaces/intersections), rather than an intentional language feature. as a result, it doesn't really work properly in most cases - see #33482

it would be nice if there was actual syntax for arrow function overloading like there is for normal functions

πŸ“ƒ Motivating Example

const foo: {
    (value: number): number
    (value: string): string
} = (value) => value

this is the most basic example of an overload i can think of, yet it doesn't work:

Type '(value: string | number) => string | number' is not assignable to type '{ (value: number): number; (value: string): string; }'.
  Type 'string | number' is not assignable to type 'number'.
    Type 'string' is not assignable to type 'number'.(2322)

as mentioned on #33482 (comment), function interfaces have a much broader purpose than overloads, meaning it has to protect against a potentially incorrect assignment in other scenarios. but here, that's obviously not what we want

perhaps a function overload type that looks something like:

const foo: overload {
    (value: number): number
    (value: string): string
} = (value) => value

πŸ’» Use Cases

makes it easier to avoid using old function syntax

@DetachHead DetachHead changed the title proper overloading for arow functions proper overloading for arrow functions Jan 31, 2022
@RyanCavanaugh RyanCavanaugh added Needs Proposal This issue needs a plan that clarifies the finer details of how it could be implemented. Suggestion An idea for TypeScript labels Feb 1, 2022
@RyanCavanaugh
Copy link
Member

This immediately runs into difficulty because how do you check the body?

  • Checking each overload independently is combinatorially explosive
  • "Combining" the return types in this case immediately produces never and the function can't legally return

@DetachHead
Copy link
Contributor Author

how do overloads work on normal functions? can it just do it the same way?

@nmain
Copy link

nmain commented Feb 1, 2022

In the normal function case, there's a separate implementation signature which is not exposed to callers, and is allowed to be very weakly typed.

@fatcerberus
Copy link

That's true @nmain, but the compiler does still check if the overloads are compatible with the implementation signature. If I read it right, the suggestion is, there would be dedicated syntax for an overload type like in the example given:

const foo: overload {
    (value: number): number
    (value: string): string
} = someFunction;

then the compiler could just use the overload compatibility check in lieu of an assignability check.

@DetachHead
Copy link
Contributor Author

DetachHead commented Mar 21, 2022

an idea i came up with to solve the issue without adding any additional syntax is to improve the logic used to infer the type of the implementation when its types are omitted.

for example:

const foo: {
    (value: number): number
    (value: string): string
} = (value) => value

currently the value is inferred as (value: string | number) => string | number which is wrong. it should be instead inferred as <T extends string | number>(value: T) => T. that way you would get no errors on assignment, and you wouldn't have to do any casting in the impl

obviously this could get messy with some more complicated overloads though:

const foo: {
    (value: number, value2: string): number
    (value: string): string
} = (value1, value2) => value

in this case perhaps there could be a built in Overload interface to simplify the inferred type of the impl:

interface Overload<Parameters extends unknown[], Return> {
    parameters: Parameters
    return: Return
}

then the impl can be inferred as something like

<O extends Overload<[number, string], number> | Overload<[string], string>>(
    ...args: O['parameters']
) => O['return']

(though while playing around with this approach i ran into other issues such as #48345 and #46076)

@icywolfy
Copy link

icywolfy commented Dec 9, 2022

@fatcerberus
Could the overload mechanism be similar to how function works?

@RyanCavanaugh Would this syntax have any glaring issues?

function foo(value: number): number;
function foo(value: string): string;
function foo(value: string|number): number|string { return value }

// idea
const foo2: (value: number) => number;
const foo2: (value: string) => string;
const foo2: (value: string|number): number|string = (value: string | number): number|string => value

@erquhart
Copy link

Probably the biggest issue is that re-declaring constants isn't valid js. The example provided in the OP is viable, though.

@icywolfy
Copy link

icywolfy commented Dec 27, 2022

Probably the biggest issue is that re-declaring constants isn't valid js. The example provided in the OP is viable, though.

@erquhart The existing function overload syntax in TypeScript declares functions without a body, which isn't valid syntax either. Invalid JS syntax is not a limitation.

@DetachHead
Copy link
Contributor Author

DetachHead commented Dec 27, 2022

imo the example in the OP is better because you don't have to duplicate the function name nor do you have to add types to the implementation signature

@erquhart
Copy link

Yeah, fair - I was more thinking of js support for declaring functions that have already been declared, and doing so with constants feels a bridge further. But yeah, not materially different since neither is really valid, agreed. Your syntax is honestly preferable if using const this way isn't an issue.

@craigphicks
Copy link

craigphicks commented Jan 6, 2024

Regardless of the syntax for arrow function overload, I can see no reason not to use the same algorithm which is used for a function overload declaration.

It's true that the function declaration algorithm would be better if the return values in the implementation were type checked against the signatures in context. We can even simulate it in user code:

function foo3(a: number): "num";
function foo3(a: string): "str";
function foo3(a: bigint): never;
function foo3(a: number | string | bigint): "num"|"str";
function foo3(a: number | string | bigint): "num"|"str" {
    if (typeof a === "string"){
        const ret = "str";
        // Conceptual simulation of type checking
        // Unfortunately we cannot write typeof foo3(a) in user code, 
        // but that lookup can be done in typescript internals.
        // Obviously would not deliberately enter infinite loop.
        const testret = foo3(a); 
        ret satisfies typeof testret;  // OK
        return ret;
    }
    else if (typeof a === "number"){
        const ret = "num";
        const testret = foo3(a);
        ret satisfies typeof testret; // OK
        return ret;
    }
    else if (typeof a === "bigint"){
        const ret = "num";
        const testret = foo3(a);
        ret satisfies typeof testret; // error
//          ~~~~~~~~~
//Type 'string' does not satisfy the expected type 'never'.(1360)
        return ret;
    }
    throw undefined;  // a==="bigint" should take this branch.
}

The obvious solution then is to unify the treatment of function declarations and assignment of arrow functions to overloads types,
and ALSO upgrade BOTH with the above return value checking. This same reasoning applies to generic functions as well.

Setting the return type of the implementation with the intersection of return types just breaks the existing type checking (which although not perfect is still better than nothing).

Also, the implementation signature could be reduced to parameter names only:

function foo3(a) { /* implementation*/ }

because it can be derived from the overloads:

readonly/const function implementation variables would help immensely with type checking function implementation return types.

Note: It would be helpful for type checking the return types if there were a way to declare the parameter names as constant. This makes flow analysis far easier. (Note only overloads, but all functions).

Even if parameters type are declared as readonly

function foo3([a,b,c,d]: readonly [a:A b:B, c:C, d:D]) { /* body */ }

the unloaded a,b,c,d are NOT const.
The only way to make use of the constness is to access the parameters are an array (or record)

function foo3([...args: readonly [a:A b:B, c:C, d:D]) { /*  args[0] cannot be written */ }

but that is unnatural for the user. This would be better:

function foo3(const [a,b,c,d]: readonly [a:A b:B, c:C, d:D]) { /* body */ }

The left hand side const is an instruction for the implemeter and implementation type checking.
The right hand side readonly is an instruction for the caller and type checking of the call.
The left hand side const implies the right hand side readonly, so it should be an error to have lhs const without rhs readonly.

Applies to objects too:

function foo4(Readonly<{a,b,c,d}>: Readonly<{a:A b:B, c:C, d:D}>) { /* body */ }

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Needs Proposal This issue needs a plan that clarifies the finer details of how it could be implemented. Suggestion An idea for TypeScript
Projects
None yet
Development

No branches or pull requests

7 participants