The reason for this behavior lies in the structural type system of TypeScript. Types in TypeScript are like "contracts" that determine compatibility between different types. The compiler only flags an error if a contract is violated.
abstract class Base{
abstract sayHello(): void;
}
class Child extends Base{
sayHello(): number {
return 123;
}
}
In this example, the method in the child class returns a number instead of void. However, due to the compatibility rule set by Microsoft, this does not cause any issues as long as the calling code expects void. The lack of side effects ensures smooth functioning despite the mismatch.
In contrast, consider:
abstract class Base{
abstract sayHello(): number;
}
class Child extends Base{
sayHello(): void {
return;
}
}
This will trigger an error since it violates the established contract of returning a number. Any code expecting a number from sayHello()
will now fail due to the broken type agreement.
This flexibility in type checking comes at the cost of potential accidental equivalences, which might lead to unexpected behavior.
Nominal type systems like C# focus on strict type equivalence, preventing scenarios where an abstract void method can be implemented to return a string. In such languages, types must match exactly for compatibility.
For more information, check out these links:
Structural type system: https://en.wikipedia.org/wiki/Structural_type_system
Nominal type system: https://en.wikipedia.org/wiki/Nominal_type_system
TypeScript Type Compatibility: https://www.typescriptlang.org/docs/handbook/type-compatibility.html
Official TypeScript specification: https://github.com/Microsoft/TypeScript/blob/master/doc/spec.md#3114-assignment-compatibility