Fundamentals 13 min read

How to Simulate Nominal Types in TypeScript: 6 Practical Techniques

This article explains what nominal type systems are, why TypeScript uses a structural system, and presents six community‑driven methods—private‑property classes, literal types, enum intersections, unique symbols, brand interfaces, and brand type intersections—to achieve nominal typing in TypeScript with code examples and pros‑cons analysis.

Taobao Frontend Technology
Taobao Frontend Technology
Taobao Frontend Technology
How to Simulate Nominal Types in TypeScript: 6 Practical Techniques

Introduction – Inspired by a quote that "programs are proofs of types," the author records insights about TypeScript’s type system, focusing on nominal (named) types.

What is a Nominal Type System?

In a nominal system, two values like userId = 123 and bookId = 34 are both numbers but are given distinct type names UserID and BookID. These types are not interchangeable because compatibility depends on the explicit type name, not just the underlying value.

Structural vs. Nominal Types

By contrast, a structural type system checks only the shape of values. For example, an object rect = { x: 33, y: 3, width: 30, height: 80 } matches the Point type because it has the required x and y properties, regardless of the type’s name.

More formal definitions can be found on the Wikipedia page for "Type system".

Nominal typing is also useful for distinguishing different strings (e.g., regex vs. HTML template), units (CNY vs. USD), and other domain‑specific identifiers.

Is TypeScript a Nominal Type System?

No. TypeScript uses a structural type system, focusing on the shape of values (often called “duck typing”).

One of TypeScript’s core principles is that type checking focuses on the shape that values have. This is sometimes called “duck typing” or “structural typing”. – TypeScript Documentation

Can TypeScript Implement Nominal Types?

TypeScript does not currently support explicit nominal type declarations and has no official plan to add them, but the community has devised several work‑arounds.

Techniques to Achieve Nominal Types in TypeScript

1. Class with a Private Property

class CNY { private __brand: void; constructor(public value: number) {} }
class USD { private __brand: void; constructor(public value: number) {} }
// Usage
const yuan = new CNY(12);
const dollar = new USD(5);
// Type safety
buyPekingDuck(dollar); // error: USD not assignable to CNY
buyCocaCola(yuan);   // error: CNY not assignable to USD

This leverages TypeScript’s rule that a type with a private member is only compatible with another type that has the same private member from the same declaration.

Pros : No extra type annotations or assertions; inference works automatically.

Cons : Requires redundant class declarations and adds a structural { value } layer; cannot be used with primitive types without extra serialization.

Recommendation : Not recommended unless you already use classes and need strict semantic separation.

2. Literal Types

type CNY = { currency: 'CNY', value: number };
type USD = { currency: 'USD', value: number };
// Usage
const yuan: CNY = { currency: 'CNY', value: 12 };
const dollar: USD = { currency: 'USD', value: 5 };
// Type safety
buyPekingDuck(dollar); // error
buyCocaCola(yuan);    // error

Distinct literal values create separate types.

Pros : Clear semantics, supports type narrowing.

Cons : Adds an extra { value } wrapper; cannot directly represent primitive types.

Recommendation : Use when you already have a structured object and the literal conveys meaning.

3. Enum Intersection

enum CNYBrand { _brand }
type CNY = number & CNYBrand;

enum USDBrand { _brand }
type USD = number & USDBrand;
// Usage
const yuan = 12 as CNY;
const dollar = 5 as USD;
// Type safety
buyPekingDuck(dollar); // error
buyCocaCola(yuan);    // error

Enum members create unique brands that intersect with primitive numbers.

Pros : Minimal runtime impact.

Cons : Requires type assertions, extra enum definitions, generates unnecessary JavaScript, and behaves differently for strings.

Recommendation : Not recommended due to added runtime cost.

4. Unique Symbol

type CNY = number & { readonly brand: unique symbol };
type USD = number & { readonly brand: unique symbol };
// Usage
const yuan = 12 as CNY;
const dollar = 5 as USD;
// Type safety
buyPekingDuck(dollar); // error
buyCocaCola(yuan);    // error

Each unique symbol is a distinct identifier, ensuring incompatibility.

Pros : No extra structure, zero runtime overhead.

Cons : Requires assertions and the unique / readonly keywords; cannot be used with generics.

Recommendation : Recommended for its simplicity and zero runtime impact.

5. Brand Interface

interface CNY extends Number { _CNYBrand: string; }
interface USD extends Number { _USDBrand: string; }
// Usage
const yuan = 12 as any as CNY;
const dollar = 5 as any as USD;
// Type safety
buyPekingDuck(dollar); // error
buyCocaCola(yuan);    // error

Extending Number with a unique brand property creates distinct types.

Pros : Works with primitive types, no runtime code, straightforward.

Cons : Requires type assertions and an any cast.

Recommendation : Very highly recommended; it is the approach used in the TypeScript source code.

6. Brand Type Intersection

type CNY = number & { _CNYBrand: string };
type USD = number & { _USDBrand: string };
// Usage
const yuan = 12 as any as CNY;
const dollar = 5 as any as USD;
// Type safety
buyPekingDuck(dollar); // error
buyCocaCola(yuan);    // error

Same idea as the brand interface but expressed with a type intersection.

Pros : Supports primitives, no extra runtime code.

Cons : Needs assertions and an any cast.

Recommendation : Very highly recommended; equivalent to the brand interface method.

Conclusion

The article introduced nominal types and demonstrated six common ways to emulate them in TypeScript, highlighting their trade‑offs. The author favors the brand interface and brand type intersection approaches for their simplicity and zero runtime cost.

Postscript

Future posts will dive deeper into the underlying mechanisms of these techniques and evaluate them against real‑world scenarios.

programmingtype systemNominal TypesStatic Typing
Taobao Frontend Technology
Written by

Taobao Frontend Technology

The frontend landscape is constantly evolving, with rapid innovations across familiar languages. Like us, your understanding of the frontend is continually refreshed. Join us on Taobao, a vibrant, all‑encompassing platform, to uncover limitless potential.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.