Search code examples
typescripttypescript-generics

How to check if an input is a branded type in TypeScript?


I feel like I'm fundamentally misunderstanding branded types here. Here's my code so far:

type BrandedString = string & { __brand: true };

function brandString(value: string): BrandedString {
  return value as BrandedString;
}

function isBrandedString(value: string): value is BrandedString {
  return (value as any)["__brand"];
}

let x = brandString("hello");
console.log("brandString", x, isBrandedString(x)); // returns false

If I hover over 'x', it does show (in intellisense) that the type is now BrandedString, but if I try to invoke my attempt at a type-guard "IsBrandedString", I get false.

I want to implement isBrandedString so that I can check for whether an input is of a brandedType or not and I can't seem to get it. Things I've tried:

...
return (value as any)["__brand"] === true
return (value as any)["__brand"]
return (value as any).__brand !== undefined;
return value[__brand] !== undefined; // compilation error

How can I check if a given input is a BrandedString?


Solution

  • TypeScript's type system is largely structural as opposed to nominal. That means two types that have identical structure are considered to be identical types. It doesn't matter if the two types have different names or were declared in different places. TypeScript lets you make type aliases, but these are just a different name for an existing type, and don't serve to distinguish the types or create a new type. It's just another name for the same thing:

    type MyString = string;
    const x: string = "abc";
    const y: MyString = x; // okay, no difference between string and MyString
    

    But sometimes people want there to be nominal types in TypeScript. You can simulate nominal typing by adding some random distinguishing structure to the type, even if that structure doesn't actually correspond to anything at runtime.

    For primitives like string, you can't even add such a structure at runtime, because they don't actually hold properties (they appear to have methods and properties via auto-boxing, but this just gives you the stuff on the prototype of the wrapper class, like String). But that doesn't stop us from writing such a type as if it did exist, and that gives us branded primitives:

    type MyString = string & { __myString: true };
    const x: string = "abc";
    const y: MyString = x; // error!
    //    ~ <-- Type 'string' is not assignable to type 'MyString'.
    

    Now the MyString type is structurally distinct from string, in that it supposedly has a __myString property of type true. And so you can no longer accidentally assign a string to a MyString. There's nothing special about __myString or true, either. It's just something random you put in there to distinguish it from other things. If you want to create other branded string types, you'd pick a different brand property.

    This is all fine, except we can't actually get a value of type MyString at runtime. Primitives can't hold properties the way objects can:

    y.__myString = true; // you can write this, but it doesn't work
    // 💥 TypeError! can't assign to property "__myString" on "abc": not an object 
    

    You can lie to to the compiler that you've got one of these branded primitives by using a type assertion:

    const z: MyString = "def" as MyString; // okay
    

    and this lets you move around values as if they were branded, but there is zero runtime effect. The entire TypeScript type system is erased upon compilation, so as MyString disappears. TypeScript has type assertions and not type casting (or at least, the sort of casting it does have should not be confused with casting in other languages like Java or C which actually does something at runtime).

    Therefore the only use of a branded primitive is to help developers keep track of the different uses of the same underlying primitive at compile time. There is no runtime test you can perform to determine if a value is branded. No runtime primitive is actually branded.


    So your brandString function

    function brandString(value: string): BrandedString {
      return value as BrandedString;
    }
    

    just returns its input and has no runtime effect. You cannot write an isBrandedString() type guard function that works:

    function isBrandedString(value: string): value is BrandedString {
      // impossible
    }
    

    The whole point of something like BrandedString is to stop you from accidentally mixing up your different "types" of strings in your own code. It's more like a mental tag you add to the type to help you keep track of things. The tag exists only in your conception of the value, not on the actual value.


    So now you need to decide why you're doing this. If you need to actually tag a value at runtime so you can distinguish a string from a BrandedString at runtime, then you pretty much need to abandon primitives. You could use an object type like

    type BrandedString = { value: string; __brand: true };
    
    function brandString(value: string): BrandedString {
        return { value, __brand: true };
    }
    
    function isBrandedString(value: unknown): value is BrandedString {
        return !!value && typeof value === "object"
            && "__brand" in value && value.__brand === true;
    }
    
    let x = brandString("hello");
    console.log("brandString", x, isBrandedString(x)); // returns true
    

    But of course now you're just holding a string in a container, and you brand the container, not the string. This might not be what you wanted to do, but it has the advantage of actually working.

    Playground link to code