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.");
}
}
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.