Search code examples
typescriptinterfacenarrowing

Why class method argument can be narrower than that of interface


Why is this code allowed in TS?

interface IFoo {
    foo(a: string | number): void
}

class Bar implements IFoo {
    foo(a: string) { }
}

I think the compiler should throw error, as param a: string of Bar is narrower/not substitutable to a: string | number of IFoo.


Solution

  • This is deliberate design and intended behavior due to Function Parameter Bivariance.

    When comparing the types of function parameters, assignment succeeds if either the source parameter is assignable to the target parameter, or vice versa. [...]

    https://www.typescriptlang.org/docs/handbook/type-compatibility.html#function-parameter-bivariance

    Essentially, this means that function types are checked in both ways. The class implementation of foo is not only assignable to the declaration in IFoo if its type is more narrow but also if it's wider. A type error only occurs if neither the interface declaration nor the class type implementation are assignable to one another.

    interface IFoo {
      foo(a: string | number): void;
    }
    
    class Bar implements IFoo {
      foo(a: string) {}
    }
    
    class Baz implements IFoo {
      foo(a: string | number | boolean) {}
    }
    
    class Invalid implements IFoo {
      foo(a: string | boolean) {}
    //~~~ Property 'foo' in type 'Invalid' is not assignable...
    }
    

    You can prevent this by enabling --strictFunctionTypes. However, there is another important thing to notice. The strict parameter type checking only works when declaring foo as an arrow function (foo: (a: string | number) => void;).

    During development of this feature, we discovered a large number of inherently unsafe class hierarchies, including some in the DOM. Because of this, the setting only applies to functions written in function syntax, not to those in method syntax.

    https://www.typescriptlang.org/tsconfig/#strictFunctionTypes

    Now you get your expected parameter type checking:

    interface IFoo {
      foo: (a: string | number) => void; // <- !! function syntax
    }
    
    class Bar implements IFoo {
      foo(a: string) { }
    //~~~ Property 'foo' in type 'Bar' is not assignable...
    }