Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why can't I "implements" an all-optional interface in TypeScript 2.4+?

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?

like image 954
Ryan Cavanaugh Avatar asked Mar 08 '23 17:03

Ryan Cavanaugh


1 Answers

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.

like image 154
Ryan Cavanaugh Avatar answered May 12 '23 16:05

Ryan Cavanaugh