Overview

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.

FAQ

What does “subtyping” mean in this chapter?A type S is a subtype of T if a value of S can be safely used anywhere a value of T is expected (Liskov substitution). This lets us reason about when one value can replace another without changing the program’s behavior.
How do nominal and structural subtyping differ, and which one does TypeScript use?Nominal subtyping requires an explicit declaration (for example, class Triangle extends Shape). Structural subtyping considers a type a subtype if it has the required members, regardless of declarations. TypeScript uses structural subtyping.
Why were Ns and Lbfs interchangeable, and how do unique symbols fix that?With only a value: number member, Ns and Lbfs have identical structure, so TypeScript treats them as mutually substitutable. Adding a unique symbol keyed property (for example, [NsType]: void) gives each type a distinct shape, simulating nominal subtyping and preventing accidental interchange.
When is structural subtyping helpful, and when is it risky?It’s helpful when you need to interoperate with external types you don’t control (for example, using a library’s User wherever a Named is expected). It’s risky when similar shapes shouldn’t be interchangeable (for example, different units of measure), where you should simulate nominal subtyping (unique symbol) to avoid mistakes.
What are the “top” and “bottom” types in TypeScript?The top type holds any value; in TypeScript that’s unknown (effectively Object | null | undefined under strictNullChecks). The bottom type is a subtype of all types; in TypeScript that’s never, which has no values and is useful for functions that never return (throw or loop).
How do unknown and any differ for safe deserialization?Both can store any value, but unknown forces you to check/refine before use (for example, via a type predicate user is User). any bypasses type checking entirely, allowing unsafe use and masking errors (for example, “Hi undefined!”). Prefer unknown for JSON.parse results and gate usage with runtime checks.
How do union (sum) types relate by subtyping?A smaller union is a subtype of a larger union: Triangle | Square is a subtype of Triangle | Square | Circle. You can pass a Triangle | Square where Triangle | Square | Circle is expected, but not vice versa.
Are arrays and generic collections covariant in TypeScript?Yes: if Triangle is a subtype of Shape, then Triangle[] is a subtype of Shape[], and LinkedList is a subtype of LinkedList. Some languages differ (for example, C#’s List is invariant unless variance is explicitly declared on interfaces).
What is function variance for returns and parameters?Returns are covariant: () => Triangle is a subtype of () => Shape. Parameters are contravariant in most languages, but TypeScript is bivariant for historical/ergonomic reasons: (arg: Shape) => void and (arg: Triangle) => void are mutually substitutable, which can permit runtime errors if misused.
How and why do we use never in error-handling flows?Functions that never return (for example, always throw) can return never. Because never is a subtype of all types, you can return fail("msg") in a function that must return number, satisfying the type checker. If fail later changes to return, the compiler forces you to fix call sites.

pro $24.99 per month

  • access to all Manning books, MEAPs, liveVideos, liveProjects, and audiobooks!
  • choose one free eBook per month to keep
  • exclusive 50% discount on all purchases
  • renews monthly, pause or cancel renewal anytime

lite $19.99 per month

  • access to all Manning books, including MEAPs!

team

5, 10 or 20 seats+ for your team - learn more


choose your plan

team

monthly
annual
$49.99
$499.99
only $41.67 per month
  • five seats for your team
  • access to all Manning books, MEAPs, liveVideos, liveProjects, and audiobooks!
  • choose another free product every time you renew
  • choose twelve free products per year
  • exclusive 50% discount on all purchases
  • renews monthly, pause or cancel renewal anytime
  • renews annually, pause or cancel renewal anytime
  • Programming with Types ebook for free
choose your plan

team

monthly
annual
$49.99
$499.99
only $41.67 per month
  • five seats for your team
  • access to all Manning books, MEAPs, liveVideos, liveProjects, and audiobooks!
  • choose another free product every time you renew
  • choose twelve free products per year
  • exclusive 50% discount on all purchases
  • renews monthly, pause or cancel renewal anytime
  • renews annually, pause or cancel renewal anytime
  • Programming with Types ebook for free
choose your plan

team

monthly
annual
$49.99
$499.99
only $41.67 per month
  • five seats for your team
  • access to all Manning books, MEAPs, liveVideos, liveProjects, and audiobooks!
  • choose another free product every time you renew
  • choose twelve free products per year
  • exclusive 50% discount on all purchases
  • renews monthly, pause or cancel renewal anytime
  • renews annually, pause or cancel renewal anytime
  • Programming with Types ebook for free