realh
realh

Reputation: 1131

How to prevent an overloaded function from being called with the wrong argument types

I want to generate typescript definitions for some JS code which is a bit awkward because some classes have static methods with the same name as their parents' static methods but different type signatures. More details here and here.

My attempt at a solution is to use a generic overload like this:

export class Parent {
    ...
    static new(arg1: string): Parent
}

export class Child extends Parent {
    ...
    static new(arg1: number, arg2: number): Child
    static new<T, V>(arg1: T): V
}

This could be abused by calling the generic form with an arbitrary type for T, causing undefined behaviour. Is there a way to prevent that? Typescript insists on all overloads with the same name having the same visibility, so I can't make the generic one private. But if there's no way of restricting calls to one of the signatures with fixed types it seems like we've missed out on one of the best benefits of overloads.

Upvotes: 0

Views: 303

Answers (1)

jcalz
jcalz

Reputation: 328453

No matter what you do with the overload signatures, TypeScript has decided that static methods will inherit from parent classes (as possibly implied by ES2015), so you can't just extend a class and work around this. At least until and unless TS ever changes. For example:

namespace Original {
  declare namespace Classes {
    class Parent {
      static method(arg1: string): Parent;
      parentProp: string;
    }

    class Child extends Parent {
      static method(arg1: number, arg2: number): Child;
      static method<T, V>(arg1: T): V;
      childProp: string;
    }
  }

  const child = new Classes.Child();
  const parent = new Classes.Parent();
  const alsoParent: typeof parent = new Classes.Child();

  const childConstructorMethod = Classes.Child.method;
  const parentConstructorMethod = Classes.Parent.method;

  // the following inheritance should hold
  const alsoParentConstructorMethod: typeof parentConstructorMethod = childConstructorMethod;
  // which leads to this even without generics
  alsoParentConstructorMethod("oopsie-daisy");
}

That is similar to your classes, and you can see that the above will end up letting people call the parent-signature method on the child constructor no matter what you do... because one can assign the child static method to a variable of the type of the parent static method. That's inheritance, and it places a restriction on things that you can't work around with signatures.


So what can you do? I'd consider changing the declared types so as not to use class at all. Note that this doesn't mean the JS can't use class; it just means that the best way to represent this sort of relationship is with your own types and values. For instance:

namespace PossibleFix {
  declare namespace Classes {
    interface Parent {
      parentProp: string;
    }
    export const Parent: {
      method(arg1: string): Parent;
      new (): Parent;
    };

    interface Child extends Parent {
      childProp: string;
    }

    export const Child: {
      method(arg1: number, arg2: number): Child;
      new (): Child;
    };
  }

  const child = new Classes.Child();
  const parent = new Classes.Parent();
  const alsoParent: typeof parent = new Classes.Child();

  const childConstructorMethod = Classes.Child.method;
  const parentConstructorMethod = Classes.Parent.method;

  // now there is no implied inheritance on the static side
  const alsoParentConstructorMethod: typeof parentConstructorMethod = childConstructorMethod;
  //    ~~~~~~~~~~~~~~~~~~~~~~~~~~~ <-- error! not assignable

}

This is nearly identical to the original class based type. A class has an instance side and a static side. Here, the instance side becomes an interface, and the static side becomes an exported const variable which is both a constructor (new():...) and has the method. But here, even though the instance side inherits (we still have Child extends Parent), the static sides are now unrelated to each other. And that allows you to pick any signatures you want on the methods of the child constructor.

This will end up being more verbose, especially if you have actual TypeScript code that was generating the original declarations, since now you have to come up with a way to assert that the generating code conforms to the latter declarations, and it could be messy. But, it gives you more freedom. Hopefully that helps a bit. Good luck!

Link to code

Upvotes: 1

Related Questions