Compare: new Square(side) new Rectangle(width, height)
In this way, a Rectangle is more like a child of a square: widening its capabilities.
Compare: new Square(side) new Rectangle(width, height)
In this way, a Rectangle is more like a child of a square: widening its capabilities.
This is not just abstract... I forget where in the docs TypeScript starts talking about certain things being incorrect because other things are not contravariant/covariant/invariant, but yeah: TypeScript is trying to implement a practical subtyping system and technically subclasses are not subtypes of their parent classes in TypeScript’s original vision, so pragmatism forces you to say “just pretend they are!” and technically the subtype system is unsound.
The problem is that in TypeScript’s theory a type is just the full interface of object properties on a value, including all their methods. Methods always look like functions with an implicit "this" argument type. So this becomes a problem precisely because the type of Rectangle looks like
type Rectangle = {
height: number,
width: number,
someMethod: (this: Rectangle) => something
}
And that recursive definition in terms of having those methods makes a type technically terminal (no subtypes can exist).So it is precisely that we can overload methods with implementations more specific to the subtype, which stops the subclass from being a subtype of the superclass.
Square(side) : Rectangle(side, side)
There's not a well-behaved way to go in the other direction. Rectangle(width, height) : Square(width) // ???
Rectangle(width, height) : Square(height) // ???That's precisely the problem. Subclasses have to less restrictive than their parents. If a subclass adds observable restrictions, it fails the LSP.
Subclasses can really only add orthogonal properties, such that influencing one of these properties doesn't perturb properties observable from the super-interface.
That's not a given. If the new capability is only in new methods of the subclass, then the program expecting the parent class will never be impacted by it.
To be fair, I suppose you agree:
> such that influencing one of these properties doesn't perturb properties observable from the super-interface.
It's worth noting that even if this capability can only occur due to methods on the subclass alone, it can still fail LSP if those manipulations are still observable to a client using only the superclass.
This is pretty much only possible because multiple clients can have a reference to the same object, since it allows one client to observably transition the object into a state that should be unreachable from the perspective of the second client. (This is the root of the need to make "defensive copies" [1].)
If one imposes a strict single-ownership discipline, then it doesn't matter if super-interface properties can be violated by subclass-only methods, because for the duration of its use via its super-interface, those violations are impossible (and hence unobservable).
Yes, but this will violate the Liskov Substitution Principle, which (effectively) states that any program that is correct for a class (Square) should be correct for the subclass (Rectangle).
So a program that takes in a parent class (Square) can rely on the sides being equal. Now pass in a child class (Rectangle) and your program may break. Hence the violation.
If you know you are constructing a Rectangle, then you use it's constructor. If you create a Square, you use that constructor.
But they are not inherited, and can have completely different semantics - accept different arguments or throw for for new reasons.
That having said, the Rectangle / Square example doesn't really work: Is `new Rectangle(42, 42)` a Square? (This brings us to factory methods...)