3

TypeScript recognises static class methods as valid properties that adhere to an interface when passing the class object as an argument, however it does not like them when the same interface is used to implement the class.

E.g. In the example below, TypeScript accepts that the Dog class adheres to the AnimalMethods interface when it is passed as an argument inside the callAnimalMethods function, however upon attempting to constrain the Dog class structure by using implements AnimalMethods, the following error is given:

Class 'Dog' incorrectly implements interface 'AnimalMethods'.
  Property 'walk' is missing in type 'Dog' but required in type 'AnimalMethods'

Example:

// error here
class Dog implements AnimalMethods {
  public static walk(): string {
    return 'walk';
  }
}

interface AnimalMethods {
  readonly walk: () => string,
}

function callAnimalMethods(animalObj: AnimalMethods): void {
  animalObj.walk();
}

callAnimalMethods(Dog);

Could someone explain why TypeScript does not allow static methods like this to adhere to an interface, especially when the structure is recognised when using the class in an argument.

Playground link.

4
  • 4
    When you have class Foo implements Bar {} it means an instance of Foo can be used as a Bar. In your example, new Dog().walk() doesn't work, so Dog does not implement AnimalMethods. TS doesn't have a way to annotate that a class's static side implements an interface; see github.com/microsoft/TypeScript/issues/33892 for a feature request to do so. If it did, then you could maybe say class Dog implements static AnimalMethods or something. But it doesn't so we can't. Does this address your question fully (and I can write an answer) or am I missing something? Commented May 24, 2022 at 20:17
  • I see where you are coming from with this. I read the discussion and the only thing that makes sense is implementation of newer syntax, which given how things usually work - would take years to be part of a stable release. The problem in my example is something that I've personally been running into for many years and the fact that TypeScript did recognise the methods when passed in as an argument made me question if there was a way. Thankyou for the comment @jcalz! You can make it an answer and I'll accept it. Commented May 24, 2022 at 22:36
  • 1
    But it does show the way in the first post of that discussion: const Dog: AnimalMethods = class Dog { public static walk() ... } which works Commented May 24, 2022 at 23:08
  • That is an interesting workaround, it seems to explicitly work for static methods and not instance methods which are common for classes though. A really nice workaround nevertheless! Commented May 25, 2022 at 8:48

1 Answer 1

4

An implements clause on a class declaration tells the compiler to check that instances of the class are assignable to the implemented type. You use implements to describe class instances, not the class constructor.

That is, something like

class Foo implements Bar { /* ... */ }

will compile without error if

const bar: Bar = new Foo();

compiles without error (modulo constructor arguments). In your example, the following are both errors:

const dog: AnimalMethods = new Dog(); // error
new Dog().walk() // error

So an instance of the Dog class is not a valid AnimalMethod, so class Dog implements AnimalMethods {} is and should be an error.


There is currently no analogous clause to say that the class constructor implements an interface. There's a longstanding open request at microsoft/TypeScript#33892 asking for something like this. If and when such a feature is ever implemented, it might look like:

// Not valid TS as of 4.6, don't write this:
class Dog implements static AnimalMethods {
    public static walk(): string {
        return 'walk';
    }
}

But for now it's not available to us. All we have are workarounds. And these workarounds are all similar to something like

callAnimalMethods(Dog);

That is, we can get the compiler to tell us whether or not the Dog value itself is assignable to AnimalMethods. You can't get this check right on the class Dog {} line, but you can get it close. For example:

type DogCheck = Implements<AnimalMethods, typeof Dog>; // okay
class Dog {
    public static walk(): string {
        return 'walk';
    }
}

where Implements is just a dummy helper type like

type Implements<T, U extends T> = void;

And you can see that it gives errors if the static side fails to satisfy the to-be-implemented interface:

type CatCheck = Implements<AnimalMethods, typeof Cat>; // error!
// -------------------------------------> ~~~~~~~~~~
//   Property 'walk' is missing in type 'typeof Cat'
class Cat {
    public static talk(): string {
        return 'meow';
    }
}

Playground link to code

Sign up to request clarification or add additional context in comments.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.