Home > other >  Why does this arrow function in an interface not compile?
Why does this arrow function in an interface not compile?

Time:12-09

What is the difference between Arrow Functions and Regular Functions in implementing Interfaces, so that code A causes compile-time error and code B compiles successfully.

Note: in tsconfig.json all strict type-checking options are enabled, including strictFunctionTypes, BTW it supposed that by enabling strict all strict type-checking options get enabled.

Code A that causes compile time error

interface SomeInterface {
    someFunction: (par1: string | undefined) => string;
}

class SomeClass implements SomeInterface {
    someFunction(par1: string): string    //invalid function signature
    {
        throw new Error('Method not implemented.');
    }
}

And, Code B that compiles successfully.

interface SomeInterface {
    someFunction(par1: string | undefined): string;
}

class SomeClass implements SomeInterface {
    someFunction(par1: string): string    //invalid function signature
    {
        throw new Error("Method not implemented.");
    }
}

Playground Link

CodePudding user response:

With --strictFunctionTypes enabled, function types' parameters are checked contravariantly, as required to maintain type safety:

class SomeClass implements SomeInterface { // error here
    someFunction(par1: string): string  
    {
        return par1.toUpperCase();
    }
}

const i: SomeInterface = new SomeClass(); // error here
i.someFunction(undefined); // runtime error here, par1 is undefined

But, as mentioned in the documentation:

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.


So method types' parameters are still checked bivariantly, meaning both contavariantly and covariantly, in order to support some common patterns (although perhaps generics or polymorphic this would be a better way to do it for some of these cases). The big example is Array<T>, where people apparently like their covariant arrays:

interface Animal { species: string }
interface Dog extends Animal { bark(): void };
const scooby: Dog = { species: "dog", bark() { console.log("ROOBY ROO or whatevz") } };
const snoopy: Dog = { species: "dog", bark() { console.log("...") } };
function processAnimals(arr: Animal[]) {
    console.log("I got "   arr.map(x => x.species).join(", ")   ".")
};
const dogs = [scooby, snoopy];
processAnimals(dogs); // okay

This is ergonomic and common, but technically the compiler should reject dogs because Dog[] would not be a valid Animal[] (indeed, methods like push() will do bad things like push a Cat into a Dog[] under the hood). But if you go down this route you find out that TypeScript is unsound all over the place in exactly this way, even without function parameters, because property writes are also like this. See this Q/A for more elaboration.


And that means your SomeClass2 produces no error since SomeInterface2 uses method syntax:

class SomeClass2 implements SomeInterface2 { // no error
    someFunction(par1: string): string {
        return par1.toUpperCase();
    }
}

Of course this has exactly the same soundness problem as before:

const i2: SomeInterface2 = new SomeClass2(); // no error
i2.someFunction(undefined); // runtime error, par1 is undefined

But that's just the way it is. Methods are less safe than functions by design, for convenience.

Playground link to code

  • Related