7 Subtyping
This chapter explains subtyping as the principle that a value of a subtype can safely stand in for a value of its supertype. It distinguishes nominal subtyping (explicit declarations like extends/implements) from structural subtyping (compatibility by shape), highlighting that TypeScript is structural. Because structural typing can allow unintended substitutions, the chapter shows how to simulate nominal subtyping in TypeScript with unique symbol-branded properties, using the Mars Climate Orbiter units-of-measure example to prevent mixing compatible-looking but semantically different types.
The text then explores the extremes of the type hierarchy. For safe deserialization, it recommends converting results like JSON.parse to unknown (the top type in TypeScript), forcing explicit runtime checks and narrowing before use—contrasted with any, which bypasses type safety. On the other end, it presents never as the bottom type: a subtype of all types that has no values, useful for functions that never return (e.g., centralized error paths). Returning the result of a fail function typed as never lets code type-check without dummy returns and ensures future changes that violate “never” are caught by the compiler.
Finally, the chapter surveys variance—how subtyping propagates through type constructors. For union (sum) types, a smaller union is a subtype of a larger union (Triangle | Square is a subtype of Triangle | Square | Circle). Collections in TypeScript such as arrays and generic lists are covariant (Triangle[] is a subtype of Shape[]), whereas some languages choose invariance. Functions are covariant in return types; for parameters, most languages use contravariance, but TypeScript allows bivariance for ergonomic reasons, which can admit runtime errors if subtype-specific operations are assumed. The overarching guidance is to understand your language’s variance rules to know when direct substitution is safe versus when adapters, checks, or transformations are required.
A top type is the supertype of any other type. We can define any number of types, but any of them would be a subtype of the top type. We can use a value of any type wherever the top type is expected.
A bottom type is the subtype of any other type. We can define any number of types, but any of these would be a supertype of the bottom type. We can pass a value of the bottom type wherever a value of any type is required (though we can never produce such a value).
Triangle | Square is a subtype of Triangle | Square | Circle. That’s because whenever a triangle, square, or circle is expected, we can use a triangle or a square.
If Triangle is a subtype of Shape, then an array of triangles is a subtype of an array of shapes. If we can use a Triangle as a Shape, we can use an array of Triangle objects as an array of Shape objects.
If Triangle is a subtype of Shape, we can use a function which returns a Triangle instead of a function which returns a Shape. That’s because we can always assign a Triangle to a caller which expects a Shape.
If Triangle is a subtype of Shape, we can use a function which expects a Shape as argument instead of a function which expects a Triangle as argument. That’s because we can always pass a Triangle to a function which takes a Shape.
If Triangle is a subtype of Shape, in TypeScript, a function which expects a Triangle can be used instead of a function which expects a Shape and vice-versa, a function which expects a Shape can be used instead of a function which expects a Triangle.
Programming with Types ebook for free