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?
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:
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, likeString). But that doesn't stop us from writing such a type as if it did exist, and that gives us branded primitives:Now the
MyStringtype is structurally distinct fromstring, in that it supposedly has a__myStringproperty of typetrue. And so you can no longer accidentally assign astringto aMyString. There's nothing special about__myStringortrue, 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
MyStringat runtime. Primitives can't hold properties the way objects can:You can lie to to the compiler that you've got one of these branded primitives by using a type assertion:
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 MyStringdisappears. 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
brandStringfunctionjust returns its input and has no runtime effect. You cannot write an
isBrandedString()type guard function that works:The whole point of something like
BrandedStringis 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
stringfrom aBrandedStringat runtime, then you pretty much need to abandon primitives. You could use an object type likeBut of course now you're just holding a
stringin 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