To me the real benefit of branded types is for branded primitives. That helps you prevent mixing up things that are represented by the same primitive type, like say relative and absolute paths, or different types of IDs.<p>You really don't need the symbol - you can use an obscure name for the branding field. I think it helps the type self-document in errors and hover-overs if you use a descriptive name.<p>I use branding enough that I have a little helper for it:<p><pre><code> /**
* Brands a type by intersecting it with a type with a brand property based on
* the provided brand string.
*/
export type Brand<T, Brand extends string> = T & {
readonly [B in Brand as `__${B}_brand`]: never;
};
</code></pre>
Use it like:<p><pre><code> type ObjectId = Brand<string, 'ObjectId'>;
</code></pre>
And the hover-over type is:<p><pre><code> type ObjectId = string & {
readonly __ObjectId_brand: never;
}</code></pre>
Anytime I’ve come across the need to do this, I’ve found a class is a better and less complicated solution.<p>I really like the pattern of value objects from Domain Driven Design. Create a class that stores the value, for example email address.<p>In the class constructor, take a string and validate it. Then anywhere that you need a valid email address, have it accept an instance of the Email class.<p>As far as I understand classes are the only real way to get nominal typing in TypeScript.
An example of what you can implement on top of branded types that I want to share with fellow hackers:<p>- currencies<p>You may have some sort of integer representing the number of cents of some currency and you want to avoid doing operations between the wrong currencies (such as adding euros and pesos).<p>You can create branded types and functions that work on those Euro branded numbers and then decide how to do math on it. Or numbers representing metals and such.<p>It's useful in other scenarios such as a, idk, strings, you could theoretically brand strings as idk ASCII or UTF-8 or the content of an http body to avoid mixing those up when encoding but I want to emphasize that often many of those hacks are <i>easier</i> to be handled with stuff like dictionaries or tagged unions.<p>An example of what can be achieved with similar approaches (beware it's oriented for people that are at least amateur practitioners of functional programming) is Giulio Canti's (an Italian mathematician and previously author of t-comb, fp-ts, io-ts, optic-ts, and now effect and effect/schema), the money-ts library:<p><a href="https://github.com/gcanti/money-ts">https://github.com/gcanti/money-ts</a>
I can see this being useful and it seems about as neat a solution as you can currently get in TypeScript as it stands today, but it’s still cumbersome.<p>My feeling is that while you can do this, you’re swimming upstream as the language is working against you. Reaching for such a solution should probably be avoided as much as possible.
Isn't this just the classic issue of inferred typing coming back to bite us in the way everyone originally predicted? Go runs into the same issue where wildly different types may be considered the same based purely on coincidental naming and matching against interfaces the original authors had no intent to match against. At the end of the day I think the easier system to work with is one in which all type compatibility needs to be explicitly declared - if your array is iterable defining that it implements iterable lets your compiler shout at you if iterable suddenly gets another abstract method that you didn't implement - and it makes sure that if you add a method `current` to a class it doesn't suddenly means that it properly supports iterable-ity.<p>Determining types by what things appear to do instead of what they state they do seems like a generally unnecessary compromise in typing safety that isn't really worth the minuscule amount of effort it can save.
Zod has branded types: <a href="https://zod.dev/?id=brand" rel="nofollow">https://zod.dev/?id=brand</a>
it works really nicely with Zod schemas
Weird idea, as types in TS are structural by <i>design</i>. If this is something you need, it smells like "runtime checking" not amending the type system.
This is also useful for having a type that needs to be verified in some way before being used or trusted. UnverifiedLoginCookie vs. VerifiedLoginCookie
It's a clever trick, but the compiler errors leave a lot to be desired. If a TS library makes heavy use of nominal (branded/distinct) types in a domain where accidentally passing values of the wrong type is common, I can imagine a lot of library users being more confused, not less, by code that uses this approach.<p>The article reads more like an endorsement of languages that do structurally-aware nominal typing (that is, languages that are nominally typed but that have awareness of structural equivalence so that zero-cost conversions and intelligible compile-time errors for invalid conversions are first class) than a persuasive case for the symbol-smuggling trick described.
The implementation for `RemoveBrand` is incorrect: it currently grabs all property types of `T`, it's not removing the property `[brand]` from `T`. It should be `Omit<T, typeof brand>`
Selfish plug about the same topic <a href="https://dnlytras.com/blog/nominal-types" rel="nofollow">https://dnlytras.com/blog/nominal-types</a>
The Odin language has a ‘distinct’ type-qualifier which accomplishes this tersely. It’s a big part of the type system, and C’s type-system for that matter.
Ah, the magical disappearing type system - now being used for nominal typing.<p>I'm curious to see what the JS code looks like for casts and type checks in that case.
Anyone else sometimes get more sucked in to perfectly typing stuff instead of writing code.<p>I guess working in the pure logic and formalism of types is just addictive.<p>I love it.
Is there something wrong with creating opaque types using this method?<p><pre><code> type ObjectId = string & {
readonly __tag: unique
symbol }
</code></pre>
This way, we don't need to use `never`, but we still prevent the creation of a structurally equivalent type by mistake.
Reminds me of another sort of type-driven development, making invalid states unrepresentable: <a href="https://geeklaunch.io/blog/make-invalid-states-unrepresentable/" rel="nofollow">https://geeklaunch.io/blog/make-invalid-states-unrepresentab...</a>
Seems like patching earlier language architecture mistake. If you create custom types, you should not use duck typing on them. You should use typecasting, so the programmer would write something like `result = processEntityA(A(B))`
Structural typing, and especially for primitives, with these work-arounds being so clunky and not built into the standard library, was <i>the</i> reason I started picking up Rust...
This works because casts are allowed to quietly create values whose types are wrong. It would have been better if the cast added a runtime check, or at least we distinguish sound (checked) and unsound casts the way C++ does.<p>I think Haskell avoid this by actually requiring you to write sound conversion functions between phantom types (it helps that phantom types don't involve any visible state at runtime).
Flow is much better with opaque types.<p>Also nominal types for classes.<p>And correct variance.<p>And adhering to liskov substitution principles.<p>And exact object types.<p>And spread on types matching runtime behavior.<p>And proper no transpilation mode with full access to the language.<p>And has 10x less LoC than ts.<p>ps. before somebody says "flow is dead" have a look at flow contributions [0] vs typescript contributions [1]<p>[0] <a href="https://github.com/facebook/flow/graphs/contributors">https://github.com/facebook/flow/graphs/contributors</a><p>[1] <a href="https://github.com/microsoft/TypeScript/graphs/contributors">https://github.com/microsoft/TypeScript/graphs/contributors</a>
Amazing, we have invented nominal typing in a structural typing system.<p>Sometimes I wonder what kind of programs are written using all these complicated TS types. Anecdotally, we use very simple, basic types in our codebase. The need for tricks like this (and other complicated types) is simply not there. Are we doing something wrong?