microsoft / TypeScript

TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
https://www.typescriptlang.org
Apache License 2.0
98.27k stars 12.2k forks source link

Allow overload signatures to have different access levels #58316

Open Howard-Lam-UnitedVanning opened 2 weeks ago

Howard-Lam-UnitedVanning commented 2 weeks ago

🔍 Search Terms

error TS2385: Overload signatures must all be public, private or protected

✅ Viability Checklist

⭐ Suggestion

What I would like to achieve with that is, that tsc checks, that I can only call public signatures from outside of a class, while I can also call private signatures from inside the class and protected ones from inside a child class.

Originally posted by @0815fox in https://github.com/microsoft/TypeScript/issues/7577#issuecomment-214605484

See also the full posts below: https://github.com/microsoft/TypeScript/issues/7577#issuecomment-214605484 https://github.com/microsoft/TypeScript/issues/58303

📃 Motivating Example

export default class NetworkService<R, G = undefined, Q = G, D = Q, RD=R> {
  get(query?: Q|null): Promise<AxiosResponse<R>>
  get(param: G|null, query: Q|null): Promise<AxiosResponse<R>>;
  protected get(param?: G|Q|null, query?: Q|null): Promise<AxiosResponse<R>>;
  async get(params?: G|Q|null, query?: Q|null): Promise<AxiosResponse<R>> {
  }
 }
 export default class FCNetworkService<R, G=undefined, Q=G, D=Q, RD=R> extends NetworkService<R,G,Q,D,RD> {
    override async get(params?: G|Q|null, query?: Q|null) {
        const result = await super.get(params, query);
    }
}

If I don't add an extra line

get(param?: G|Q|null, query?: Q|null): Promise<AxiosResponse<R>>;

I get the error "Argument of type 'G | Q | null | undefined' is not assignable to parameter of type 'G | null'."

But that function signature should not be ever called from the outside because the function body cannot tell if params is actually a query object if the second input is undefined, which is why I have the restriction on the second overlord. That is why I need protected for the third overload.

💻 Use Cases

  1. What do you want to use this for? For generic class inheritance overloaded functions override
  2. What shortcomings exist with current approaches? When inheriting and overriding the function with overload signatures, the final combined signature must be provided for the override to call the super method but that signature should not be exposed because it may contain input patterns that logic of the function cannot distinguish.
  3. What workarounds are you using in the meantime? I disable the error on the super method call with @ts-ignore without adding the extra signature needed. If parent changes API later on, the child will fail to detect the change super.method.apply doesn't work because I get Argument of type 'IArguments' is not assignable to parameter of type '[params: G | null, data: D]'
MartinJohns commented 2 weeks ago

Duplicate of #9592. It's fairly old tho, things might have changed. In 2021 the reasoning was still true.

RyanCavanaugh commented 2 weeks ago

It's still pretty problematic, because let's say you write this:

class Foo {
    foo(s: string): void;
    foo(n: number): void;
    protected foo(b: boolean): void;
    foo(a: any) {
    }

    bar() {
        return this.foo;
    }

    baz() {
        this.bar()("hello");
        this.bar()(true);
    }
}

const p = (new Foo()).bar();
p("hello");
p(true);

There's not really an obvious way to write down the type of bar's return type, nor is it clear if baz's invocations are both legal. Today it's a basic syntactic analysis to see if a protected or private call is legal -- we just check the lexical position of the call. But if the overloads are conditionally visible, then there's unknown new machinery to represent that from a type perspective.

Howard-Lam-UnitedVanning commented 2 weeks ago

I think in your case, it doesn't matter, all three overloads would just become public anyway if you return it with a public method. See below code, If you make all of them private, all three of the overloads are still accessible via p.

class Foo {
    private foo(s: string): void;
    private foo(n: number): void;
    private foo(n: boolean): void;
    private foo(a: any) {
    }

    bar() {
        return this.foo;
    }
}

const p = (new Foo()).bar();
p("hello");
p(true); // no problem on any of these 3