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.
CodePudding user response:
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';
}
}