้•œๅƒ็ซ™็‚น ยท ๆœฌ้กต็”ฑ็ฌฌไธ‰ๆ–น GitHub ๅช่ฏป้•œๅƒๆไพ›๏ผŒ้ž GitHub ๅฎ˜ๆ–น็ซ™็‚น๏ผŒไธๆŽฅๅ—ไปปไฝ•็™ปๅฝ•ๆˆ–ๅ‡ญๆฎ่พ“ๅ…ฅใ€‚ๅ‰ๅพ€ github.com
Skip to content

proper overloading for arrow functionsย #47669

Open

Description

@DetachHead

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

Activity

  1. changed the title [-]proper overloading for arow functions[/-] [+]proper overloading for arrow functions[/+] on Jan 31, 2022
  2. RyanCavanaugh commented on Feb 1, 2022

    @RyanCavanaugh
    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
  3. DetachHead commented on Feb 1, 2022

    @DetachHead
    ContributorAuthor

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

  4. nmain commented on Feb 1, 2022

    @nmain

    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.

  5. fatcerberus commented on Feb 1, 2022

    @fatcerberus

    That's true Nicholai Main (@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.

  6. DetachHead commented on Mar 21, 2022

    @DetachHead
    ContributorAuthor

    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)

  7. icywolfy commented on Dec 9, 2022

    @icywolfy

    Bruce Pascoe (@fatcerberus)
    Could the overload mechanism be similar to how function works?

    Ryan Cavanaugh (@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
  8. erquhart commented on Dec 26, 2022

    @erquhart

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

  9. icywolfy commented on Dec 27, 2022

    @icywolfy

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

    Shawn Erquhart (@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.

  10. DetachHead commented on Dec 27, 2022

    @DetachHead
    ContributorAuthor

    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

  11. erquhart commented on Dec 27, 2022

    @erquhart

    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.

  12. craigphicks commented on Jan 6, 2024

    @craigphicks

    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

Metadata

Metadata

Assignees

No one assigned

    Labels

    Needs ProposalThis issue needs a plan that clarifies the finer details of how it could be implemented.SuggestionAn idea for TypeScript

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions