Overview

4 Type Safety

This chapter explains how to use types to make programs safer by reducing opportunities for bugs through explicit meaning and guarantees. It warns against primitive obsession—modeling domain values as bare primitives like number or string—which invites misinterpretation (such as mixing currencies and distances). The remedy is to define small, focused domain types that encode units and intent, forcing incompatible values to be caught at compile time and improving readability. Once a value is constructed as a domain type, its meaning is unambiguous and trusted throughout the codebase.

Beyond encoding meaning, the chapter shows how to enforce invariants at construction time so invalid states can’t exist. Constraints can be checked in constructors (throwing or coercing) or in factories (returning undefined or a safe alternative), with readonly/private fields preserving validity after creation. It then covers adding type information via casts: explicit casts are powerful but dangerous when they bypass checks; they’re safe when preceded by runtime validation or when the program tracks discriminators outside the type system (as with Either’s isLeft/getLeft pattern). The chapter distinguishes implicit versus explicit casts and categorizes their safety: upcasts and widening casts are generally safe, while downcasts and narrowing casts are risky and should be made explicit and justified.

The final part addresses scenarios where we deliberately hide and later restore type information for flexibility, such as heterogeneous collections and serialization. It presents three main approaches for mixed-type collections—common base/interface, sum types (unions/variants), and unknown/any—highlighting the trade-offs between ease of use, constraints on members, and the need for downcasts or visitors. Serialization similarly strips type information; deserialization restores it by reconstructing objects and, when necessary, carrying explicit type tags alongside the data. Throughout, the guidance is to encode domain meaning and constraints in types whenever possible, hide types only when the problem demands flexibility, and use casts sparingly and responsibly to move between the two worlds.

The numeric value 1000 could represent 1000 dollars or 1000 miles. Two different developers could interpret it as two very different measures.
Having an explicit Currency type makes it clear that the value does not represent 1000 miles, rather a dollar amount.
With casting, we can turn a value of type 16-bit signed integer into a UTF-8 encoded character.
If we have either a triangle or a square, we can’t say for sure whether the actual shape we have will pass through a triangular slot. It would if it’s a triangle, but it wouldn’t if it’s a square.
Example of widening and narrowing casts. The widening cast is safe: the gray squares represent the extra bits we get, so no information can be lost. On the other hand, the narrowing cast is dangerous: the black squares represent bits which no longer fit in the new type.
If we have a bag that only contains cats, we can bet that whichever item we pull out of it will be a cat. If the bag can also contain groceries, we are no longer able to guarantee what we will pull out.
A compact car with 2 doors and front wheel drive serialized as JSON then deserialized back into a car.

FAQ

What is the “primitive obsession” anti-pattern and why is it risky?Primitive obsession is representing domain concepts with basic types (like number or string) and relying on implicit assumptions. It’s risky because different parts of the code can interpret the same primitive differently (for example, 1000 as dollars vs miles). The remedy is to create domain-specific types (e.g., Currency, Miles) so the type checker prevents misinterpretations.
How can I prevent unit mix-ups (like Ns vs lbfs) in TypeScript?Create branded domain types that wrap the primitive value (e.g., classes Ns and Lbfs with a unique symbol field) and provide explicit conversion functions (e.g., lbfsToNs). Passing an Lbfs where an Ns is expected will fail at compile time until you convert, preventing runtime mistakes.
How do I enforce value constraints (e.g., Celsius ≥ −273.15)?Enforce constraints at construction: - In a constructor, either throw on invalid input or coerce to the nearest valid value. - Or use a factory method that returns undefined (or an Optional/Result) for invalid inputs. Once created, make the inner value readonly so instances remain valid.
When should I prefer a factory over a constructor for validation?Use a factory when: - You don’t want to throw on invalid input (return undefined/Optional instead). - Construction/validation logic is complex and you want to keep constructors lightweight. - You want a single controlled place that can gate creation of valid instances.
What’s the difference between upcasts and downcasts? Which are safe?Upcasts treat a derived type as its base type (Triangle → Shape) and are safe and usually implicit. Downcasts treat a base type as a derived type (Shape → Triangle) and are potentially unsafe; they require explicit casting and often a runtime check/guard to avoid errors.
What are widening and narrowing casts, and which should I prefer?Widening casts move to a type that can represent all previous values and more (e.g., 8-bit int → 16-bit int) and are generally safe/implicit. Narrowing casts move to a type that can represent fewer values (e.g., 16-bit int → 8-bit int, or signed → unsigned) and are risky; be explicit and cautious.
Are explicit type casts in TypeScript safe? When do I need them?Explicit casts (as T or <T>value) bypass type checking and can cause runtime errors if misused (e.g., casting a Bike to a SportsCar). Sometimes you must cast via unknown to “forget” a type before asserting another. Use explicit casts only when you truly know more than the compiler and have validated the value.
How can I safely restore precise types after the compiler “forgets” them (e.g., with Either)?Track discriminating information alongside the value (e.g., a boolean or tag). Expose type-guard-like methods (e.g., isLeft()) and accessors (e.g., getLeft()) that check the tag and then cast internally. Callers check first, then retrieve, keeping runtime safe and compiler-informed.
What are my options for heterogeneous collections, and their trade-offs?Three common options: - Base type or interface (e.g., IDocumentItem[]): easy to use shared behavior; requires a common contract. - Union/sum type (e.g., (Paragraph | Picture | Table)[]): no shared contract needed; must narrow or visit to use specific members. - unknown[] (or any[]): can store anything; must track and cast back later. Prefer unknown over any for safety.
How do serialization and deserialization affect type safety?Serialization (e.g., JSON.stringify) strips type information, yielding a string/bytes. Deserialization (e.g., JSON.parse) returns any/unknown; you must reconstruct and optionally cast (e.g., Object.assign(new Cat(), parsed)). To restore types safely, include a discriminator/tag in the serialized data (e.g., prefix or a type field) and validate before casting. Prefer unknown over any and avoid calling methods before you restore the type.

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