The error you already know
You have a circle or a square, and you reach for the radius the way you always have. In Python the attribute is right there at runtime, and if you wanted to be careful you would guard it with isinstance first. Here the compiler reads the line before the program runs and refuses the guess.
type Circle = { radius: number };type Square = { side: number };// The isinstance reflex, ported// straight across.function area(s: Circle | Square): number {return Math.PI * s.radiusProperty 'radius' does not exist on type 'Circle | Square'. ** 2;}
The reflex would be to branch first, the way isinstance does. The trouble is that typeof reports "object" for both shapes, and there is no class to instanceof, so neither check can tell a circle from a square. The value carries no label the compiler can read. Give it one.
shapes.ts(8,28) Property 'radius' does not exist on type 'Circle | Square'.
Watch the habit become the idiom
The fix is one small change to the data. You add a field whose only job is to say which shape this is, and once that field exists the compiler can do the narrowing for you.
Step through it at your own pace. Each click is one move.
Step 1 of 4
Good Python. Dataclasses and isinstance, exactly what you were taught.
@dataclass
class Circle:
radius: float
@dataclass
class Rect:
width: float
height: float
def area(s):
if isinstance(s, Circle):
return pi * s.radius ** 2
return s.width * s.heightHere's what each move does.
- Give every variant one literal field, a
kind, that names it and nothing else. - Collect the variants into a union of object types, no base class above them.
- Switch on
kind, and the compiler narrows each case to the one shape that matches, sos.radiusis legal insidecase "circle"and only there. - It checks the switch is exhaustive. Miss a case and the function can return
undefined, and the compiler says so before you ship.
That last move is the one a class hierarchy never gave you. Inheritance lets a new subclass appear anywhere and quietly skip your isinstance ladder. A closed union is a promise about how many shapes exist, and the compiler holds you to it. Now the question is just what shape it is, and kind answers it.
Now make the compiler agree
The union has three shapes now, and the switch handles two. Add the triangle case so every shape returns a number and the compiler runs out of complaints. The starter leaves the parameter untyped on purpose, so give area the Shape it deserves while you are there. No classes.