I just had a nasty bug today because I put the wrong id, and looking for a good resource for introducing branded types to my project in a lightweight way. This looks nice.
Yes, I’m pretty sure these are “just” phantom types. I think maybe a distinction is that there is no value-level representation for a phantom type (i.e. erased after type checking) but brands seem to contain values in them?
I’m not sure how to compare to phantom types, but in the case of branded types, you still want e.g. a string, but want to type it nominally. In other words, it can be substituted as another primitive type, but you can’t substitute a primitive to the branded type. And it’s all erased in runtime!
Absolutely love these to keep straight milliseconds and microseconds in my work, which is otherwise rather painful.
The biggest challenge is no operator overloading, so the type is lost with any arithmetic. You’re forced to cast again, but that can easily shroud errors.
I don’t think so. I think this is an encoding of nominal types (might be newtype in Haskell?), while phantom types are specifically unused generic type parameters. But you probably need to have nominal types before phantom types are useful, because otherwise you don’t have a good way to make different phantom types compare unequally.
I guess you can encode new type as a generic data type with a single phantom parameter, which would make phantom types enough for new type… cool. So it’s not as powerful as phantom types?
Yes, effectively! It’s slightly more ergonomic in TypeScript because you don’t have to manually destructure the values when using them – the brand only exists at type level. As values, they act just like normal strings or numbers (or whatever type you decide to brand).
This really has improved the type-safety of my server code, it’s really nice to be able to pass in userId: UserId to a function and be sure that this is actually a valid user id.
I just had a nasty bug today because I put the wrong id, and looking for a good resource for introducing branded types to my project in a lightweight way. This looks nice.
Yes, I’m pretty sure these are “just” phantom types. I think maybe a distinction is that there is no value-level representation for a phantom type (i.e. erased after type checking) but brands seem to contain values in them?
I’m not sure how to compare to phantom types, but in the case of branded types, you still want e.g. a string, but want to type it nominally. In other words, it can be substituted as another primitive type, but you can’t substitute a primitive to the branded type. And it’s all erased in runtime!
Absolutely love these to keep straight milliseconds and microseconds in my work, which is otherwise rather painful.
The biggest challenge is no operator overloading, so the type is lost with any arithmetic. You’re forced to cast again, but that can easily shroud errors.
Is this the same as a phantom type in Haskell?
I don’t think so. I think this is an encoding of nominal types (might be
newtypein Haskell?), while phantom types are specifically unused generic type parameters. But you probably need to have nominal types before phantom types are useful, because otherwise you don’t have a good way to make different phantom types compare unequally.I guess you can encode new type as a generic data type with a single phantom parameter, which would make phantom types enough for new type… cool. So it’s not as powerful as phantom types?
Yes, effectively! It’s slightly more ergonomic in TypeScript because you don’t have to manually destructure the values when using them – the brand only exists at type level. As values, they act just like normal strings or numbers (or whatever type you decide to brand).
I’ve started using branded types using Effect’s Schema library:
This really has improved the type-safety of my server code, it’s really nice to be able to pass in
userId: UserIdto a function and be sure that this is actually a valid user id.