I wrote some code:
interface IEventListener {
onBefore?(name: string): void;
onAfter?(name: string): void;
}
class BaseListener implements IEventListener {
stuff() {
}
}
The intent here is that someone can derive from BaseListener
and get correct typechecking on their onBefore
/ onAfter
methods:
class DerivedListener extends BaseListener {
// Should be an error (name is string, not number)
onBefore(name: number) {
}
}
However, I don't get an error in DerivedListener
. Instead I got an error in BaseListener
:
Type "BaseListener" has no properties in common with type "IEventListener"
What's going on?
The implements
clause in TypeScript does exactly one thing: It ensures that the declaring class is assignable to the implemented interface. In other words, when you write class BaseListener implements IEventListener
, TypeScript checks that this code would be legal:
var x: BaseListener = ...;
var y: IEventListener = x; // OK?
So when you wrote class BaseListener implements IEventListener
, what you probably intended to do was "copy down" the optional properties of IEventListener
into your class declaration.
Instead, nothing happened.
TypeScript 2.4 changed how all-optional types work. Previously, any type which didn't have properties of a conflicting type would be assignable to an all-optional type. This leads to all sorts of shenanigans being allowed:
interface HttpOptions {
method?: string;
url?: string;
host?: string;
port?: number;
}
interface Point {
x: number;
y: number;
}
const pt: Point = { x: 2, y: 4 };
const opts: HttpOptions = pt; // No error, wat?
The new behavior as of 2.4 is that an all-optional type requires at least one matching property from the source type for the type to be considered compatible. This catches the above error and also correctly figures out that you tried to implements
an interface without actually doing anything.
Instead what you should do is use declaration merging to "copy down" the interface members into your class. This is as simple as writing an interface declaration with the same name (and same type parameters, if any) as the class:
interface IEventListener {
onBefore?(name: string): void;
onAfter?(name: string): void;
}
class BaseListener {
stuff() {
}
}
interface BaseListener extends IEventListener { }
This will cause the properties of IEventListener
to also be in BaseListener
, and correctly flag the error in DerivedListener
in the original post.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With