Search code examples
typescriptmethodsinterfacesubtyping

Why doesn't TypeScript complain when assigning a class to an interface that accepts more values for its parameter than the class?


Why doesn't TypeScript complain when I assign a class to an interface, where the interface is a superset of the class.

Example:

interface AI {
  hello(msg: string | number): void;
}

class A {
  hello(msg: string) {}
}

const a = new A();

const b: AI = new A(); // Why not complain here?

a.hello(1); // Argument of type 'number' is not assignable to parameter of type 'string'.
b.hello(1);


Solution

  • In TypeScript, methods are always checked bivariantly, meaning that you are allowed to widen the types of their parameters, which is safe, or to narrow the types of their parameters, which is unsafe, as shown here:

    interface Iface {
      method(msg: string | number): void; // method syntax
    }
    
    const obj: Iface = {
      method(msg: string) { msg.toUpperCase() } // okay?!
    }
    
    obj.method(1); // runtime error
    

    In this example, Iface has a method() that needs to accept string | number, but obj, which is annotated as an Iface has a method() that only accepts string. The compiler does not warn about this, and you get a runtime error. This seems like exactly the sort of thing TypeScript is supposed to avoid, but (according to the documentation) it is rare in practice for people to actually do such unsafe things with bivariant methods. Instead, people tend to do things like this:

    class Base {
      constructor(public name: string) { }
      compare(other: Base) {
        return this.name.localeCompare(other.name)
      }
    }
    
    class Subclass extends Base {
      constructor(name: string, public age: number) { super(name); }
      compare(other: Subclass) {
        return (this.age - other.age) || this.name.localeCompare(other.name)
      }
    }
    

    It's technically unsafe for Subclass's compare() method to accept only a Subclass when the BaseClass's compare() method accepts any Base. But it's very useful and common to do this sort of thing, and as long as don't upcast a Subclass to Base, you won't observe the safety hole directly.


    Anyway, if you're interested in preventing this, you can turn on the --strictFunctionTypes compiler option (part of the --strict suite of compiler options) and refactor so that any offending method declarations are rewritten as function-valued property declarations:

    interface Iface {
      method: (msg: string | number) => void; // function-valued prop syntax
    }
    
    const obj: Iface = {
      method(msg: string) { msg.toUpperCase() } // error!
      //~~~~ <-- Type 'string | number' is not assignable to type 'string'.
    }
    

    Now you will get warnings if you unsafely narrow a method parameter in an implementation. Note that you can still implement the method via method syntax; obj's method property is not an arrow function and you still get the desired error.

    Playground link to code