Skip to content

Enable returning locally defined type in signature overloadsΒ #48543

@sxxov

Description

@sxxov

Suggestion

πŸ” Search Terms

signature, method, function, overload, return, local, scope, type, define, declare, variable, anonymous, class

βœ… 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

Signature overloads should allow the user to return types of objects/functions/classes defined inside the function body, as is currently allowed for non-overloads.

πŸ“ƒ Motivating Example

Currently, using type inference, we're able to return unique types only declared locally:

export class Foo {
  something() {
    return class Bar {};
  }
}

Writing a return type is also possible by using typeof on a variable:

export class Foo {
  something(): typeof Bar {
    const Bar = class {};

    return Bar;
  }
}

However, when using signature overloading, providing no return type would not fallback to inference (returning any); attempting to provide a reference to a local variable in the return type would throw a compile error:

export class Foo {
  something(): void;
  something(): typeof Bar; // Cannot find name 'Bar'. ts(2304)
  something(): typeof Bar | void {
    const Bar = class {};

    return Bar;
  }
}

NOTE: I could only get the above return type code compiling on my local machine, the playground would throw the below error even with (from what I could tell) matching compiler versions (4.6.2) & configs.

Return type of public method from exported class has or is using private name 'Bar'

Any help in resolving this to get a valid example would be great.

πŸ’» Use Cases

Better editor completion with fallback types:

NOTE: This below example will work without the overloads, however, the editor won't suggest anything.

export class Foo {
  something<T extends 'qux' | 'quux'>(arg: T): typeof Bar;
  something<T extends string>(arg: T): typeof Bar;
  something<T extends string>(arg: T): typeof Bar {
    const Bar = class {
      public baz = arg;
      public isQux = (arg === 'qux') as
        | string extends T
        ? boolean
        : T extends 'qux'
          ? true
          : false
    };

    return Bar;
  }
}

/* -------------------- Usage -------------------- */

// typing `new Foo().something('`
// the editor would give suggestions for `'qux' | 'quux'`
// whist accepting other `string` inputs

const qux = new (new Foo().something('qux'))();
qux.baz;        // asserts qux.baz is 'qux'
qux.isQux;      // asserts qux.isQux is true

const quux = new (new Foo().something('quux'))();
quux.baz;       // asserts quux.baz is 'quux'
quux.isQux;     // asserts quux.isQux is false

const hello = new (new Foo().something('hello'))();
hello.baz;      // asserts hello.baz is 'hello'
hello.isQux;    // asserts hello.isQux is false

const string = new (new Foo().something(String()))();
string.baz;     // asserts string.baz is string
string.isQux;   // asserts string.isQux is boolean

NOTE: Currently, this behaviour is actually already possible for properties inside of objects using the & operator:

export class Foo {
  // new Foo().map.set('hello', 'works');
  map:
  | Map<'qux' | 'quux', unknown>
    & Map<string, unknown>;
}

I, however, haven't found anything for methods; if there indeed is a more elegant solution, please do ridicule & educate me (::

Differing constructed classes depending on a string argument:

export class Foo {
  something<T extends 'qux' | 'quux'>(arg: T): typeof Qux;
  something<T extends string>(arg: T):
    | string extends T
      ? typeof Qux | typeof NotQux
      : T extends 'qux'
        ? typeof Qux
        : typeof NotQux;
  something<T extends string>(arg: T): 
    | typeof Qux
    | typeof NotQux {
    const Bar = class {
      public baz = arg;
    };

    const Qux = class extends Bar {
      public override baz = arg;
      public readonly isQux = true;

      // ...more complicated props
    };

    const NotQux = class extends Bar {
      public override baz = arg;
      public readonly isNotQux = true;

      // ...more complicated props
    };

    return (arg === 'qux' ? Qux : NotQux) as
      | string extends T
        ? (typeof Qux | typeof NotQux)
        : T extends 'qux'
          ? typeof Qux
          : typeof NotQux;
  }
}

/* -------------------- Usage -------------------- */

const qux = new (new Foo().something('qux'))();
qux;              // asserts qux is Qux
qux.baz;          // asserts qux.baz is 'qux'
qux.isQux;        // asserts qux.isQux is true

const quux = new (new Foo().something('quux'))();
quux;             // asserts quux is NotQux
quux.baz;         // asserts quux.baz is 'quux'
quux.isNotQux;    // asserts quux.isNotQux is true

const hello = new (new Foo().something('hello'))();
hello;            // asserts hello is NotQux
hello.baz;        // asserts hello.baz is 'hello'
hello.isNotQux;   // asserts hello.isNotQux is true

const string = new (new Foo().something(String()))();
string;           // asserts string is NotQux
string.baz;       // asserts string.baz is string
string.isNotQux;  // asserts string.isNotQux is true

Metadata

Metadata

Assignees

No one assigned

    Labels

    Awaiting More FeedbackThis means we'd like to hear from more people who would be helped by this featureSuggestionAn idea for TypeScript

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions