Skip to content

Allow function overloads with a varying parameter to unify, and inductively narrow the parameter when narrowing the return type #12885

@dead-claudia

Description

@dead-claudia

I don't really know how to word the title for this one or explain it well...but here's what I'm thinking: it would be nice to allow function overloads to unify, provided only one parameter and possibly the return type are different across each overload. Additionally, when narrowing the returned type, it should be able to inductively narrow the overloaded parameter's type accordingly within the same block. To hopefully explain this a little better, here's what I mean:

enum Types { Foo, Bar }
class Foo { foo: number; }
class Bar { bar: number; }
declare function foo(type: Types.Foo, length: number): Foo;
declare function foo(type: Types.Bar, length: number): Bar;

// This should be okay, with `result` inferred as `Foo | Bar`
const type: Types.Foo | Types.Bar = getType();
const result = foo(type, 1);

if (result instanceof Foo) {
    // str is inferred as Types.Foo here
} else {
    // str is inferred as Types.Bar here
}

In this case, if result is a Foo, str can only possibly be a "foo" through induction, and similarly result being a Bar and str being a "bar".

With #12883 also, this would also permit assertions to do compile-time type narrowing, without adding any new syntax or special casing of any particular identifier, hence fixing #12825 and #8655 simultaneously while remaining much more flexible:

declare function assert(cond: true, message: string): void;
declare function assert(cond: false, message: string): never;

I know this would likely be really hard to implement, but it would pay off. It helps that boolean is equivalent to true | false, "foo" is a subtype of string, E is equivalent to E.A | E.B | E.C where enum E {A, B, C}, etc., so much of the structural narrowing would allow this to apply to several other areas.


Note that this would specifically not allow more than one varying type to be unified, because it would be an M⨯N type implication, which would be unrealistic to infer in practice (you would already need length to be similarly guarded in theory):

declare function foo(type: "foo", length: 1): Foo;
declare function foo(type: "bar", length: 2): Foo;

let type: "foo" | "bar" = someType;
let length: 1 | 2 = someLength;
let result = foo(type, length); // Fail

Sorry if I did a really poor job explaining it (I don't really know the correct technical term for this, hence the detailed examples).

Metadata

Metadata

Assignees

No one assigned

    Labels

    In DiscussionNot yet reached consensusSuggestionAn idea for TypeScript

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions