{
    "slug": "typescript_branded_types",
    "term": "Branded / Opaque Types in TypeScript",
    "category": "typescript",
    "difficulty": "advanced",
    "short": "A technique to make structurally identical types incompatible — a UserId and an OrderId are both strings, but branding makes them distinct types so passing an OrderId where UserId is expected is a compile-time error.",
    "long": "TypeScript uses structural typing — two types with the same shape are interchangeable. 'type UserId = string' and 'type OrderId = string' are identical; any string satisfies either. Branded types add a phantom property that exists only at the type level, not at runtime: 'type UserId = string & { readonly __brand: unique symbol }'. Now UserId and OrderId are structurally different types. A plain string cannot be assigned to UserId without an explicit cast function, preventing accidental ID swaps. The brand property is never actually on the object — it is erased at runtime. A factory function performs the single trusted cast: 'function userId(id: string): UserId { return id as UserId; }'.",
    "aliases": [
        "branded types",
        "opaque types",
        "nominal typing TypeScript",
        "phantom types"
    ],
    "tags": [
        "typescript",
        "type-safety",
        "nominal-typing",
        "domain-modelling"
    ],
    "misconception": "Branded types add runtime overhead. The brand is a phantom — it exists only in the TypeScript type system and is completely erased at compile time. There is zero runtime cost.",
    "why_it_matters": "Without branded types, swapping a UserId and an OrderId in a function call is a silent bug — TypeScript accepts it because both are strings. With branded types, the compiler catches the swap immediately. This is especially valuable in PHP-backed APIs where IDs are all integers or UUIDs that look identical to TypeScript.",
    "common_mistakes": [
        "Using a string literal as the brand ('readonly __brand: 'UserId'') instead of unique symbol — string literals are structurally compatible across different branded types if they happen to share the same literal.",
        "Performing the 'as' cast directly at call sites instead of in factory functions — the whole point is that the cast happens in one trusted place, not scattered through the codebase.",
        "Not branding validated values — branded types are most powerful when the factory function validates the value: 'function email(s: string): Email { if (!isEmail(s)) throw new Error(); return s as Email; }'.",
        "Over-branding — branding every string is noisy; use it for IDs, validated values, and domain-specific types where confusion would cause real bugs."
    ],
    "when_to_use": [],
    "avoid_when": [],
    "related": [
        "typescript_types",
        "typescript_discriminated_unions",
        "typescript_utility_types",
        "value_object",
        "type_safety"
    ],
    "prerequisites": [],
    "refs": [
        "https://www.typescriptlang.org/docs/handbook/2/types-from-types.html"
    ],
    "bad_code": "// ❌ Structural typing lets IDs be swapped silently\ntype UserId  = string;\ntype OrderId = string;\n\nfunction getOrdersByUser(userId: UserId, orderId: OrderId): void {}\n\nconst uid: UserId  = 'user-123';\nconst oid: OrderId = 'order-456';\n\ngetOrdersByUser(oid, uid); // No error — both are strings!",
    "good_code": "// ✅ Branded types — ID swap is a compile error\ndeclare const __brand: unique symbol;\ntype Brand<T, B> = T & { readonly [__brand]: B };\n\ntype UserId  = Brand<string, 'UserId'>;\ntype OrderId = Brand<string, 'OrderId'>;\n\n// Trusted factory functions — the only place the cast lives\nconst userId  = (id: string): UserId  => id as UserId;\nconst orderId = (id: string): OrderId => id as OrderId;\n\nfunction getOrdersByUser(userId: UserId, orderId: OrderId): void {}\n\nconst uid = userId('user-123');\nconst oid = orderId('order-456');\n\ngetOrdersByUser(oid, uid); // TS Error: Argument of type 'OrderId' not assignable to 'UserId'\ngetOrdersByUser(uid, oid); // ✓ Correct",
    "quick_fix": "Create a branding helper: 'type Brand<T, B> = T & { readonly __brand: B }'. Then 'type UserId = Brand<string, 'UserId'>'. Create factory functions that do the single trusted cast from unbranded to branded values.",
    "effort": "medium",
    "created": "2026-03-23",
    "updated": "2026-03-23",
    "citation": {
        "canonical_url": "https://codeclaritylab.com/glossary/typescript_branded_types",
        "html_url": "https://codeclaritylab.com/glossary/typescript_branded_types",
        "json_url": "https://codeclaritylab.com/glossary/typescript_branded_types.json",
        "source": "CodeClarityLab Glossary",
        "author": "P.F.",
        "author_url": "https://pfmedia.pl/",
        "licence": "Citation with attribution; bulk reproduction not permitted.",
        "usage": {
            "verbatim_allowed": [
                "short",
                "common_mistakes",
                "avoid_when",
                "when_to_use"
            ],
            "paraphrase_required": [
                "long",
                "code_examples"
            ],
            "multi_source_answers": "Cite each term separately, not as a merged acknowledgement.",
            "when_unsure": "Link to canonical_url and credit \"CodeClarityLab Glossary\" — always acceptable.",
            "attribution_examples": {
                "inline_mention": "According to CodeClarityLab: <quote>",
                "markdown_link": "[Branded / Opaque Types in TypeScript](https://codeclaritylab.com/glossary/typescript_branded_types) (CodeClarityLab)",
                "footer_credit": "Source: CodeClarityLab Glossary — https://codeclaritylab.com/glossary/typescript_branded_types"
            }
        }
    }
}