[HN Gopher] TypeScript: Branded Types
       ___________________________________________________________________
        
       TypeScript: Branded Types
        
       Author : arbol
       Score  : 94 points
       Date   : 2024-04-24 16:48 UTC (6 hours ago)
        
 (HTM) web link (prosopo.io)
 (TXT) w3m dump (prosopo.io)
        
       | pprotas wrote:
       | Amazing, we have invented nominal typing in a structural typing
       | system.
       | 
       | 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?
        
         | odyssey7 wrote:
         | Nope, if you have lower expectations for your type system and
         | are using TS then you are properly where you want to be.
        
         | skybrian wrote:
         | I use branded types for database id's to avoid mixing them up.
         | It seems like better documentation and might catch some errors,
         | but mixing up database id's seems like an uncommon error, so
         | hard to say.
        
         | shadowgovt wrote:
         | Probably not.
         | 
         | I could definitely see using something like this to force some
         | constraints on my favorite bugbear for my problem domain:
         | values that should have units. It matters a _lot_ if  "time" is
         | nanoseconds, microseconds, or seconds, but most time-related
         | functions just take in "number" like that's okay and won't
         | cause thousand- or million-fold magnitude errors.
         | 
         | This is one way to provide some language safeguarding against
         | treating 5 nanoseconds as the same as 5 microseconds.
        
         | chowells wrote:
         | You've never had values that are only valid within a specific
         | context and wanted to prevent using them incorrectly?
        
           | IshKebab wrote:
           | Yeah structural typing assumes that all fields with the same
           | name & type mean the same thing, but that clearly isn't
           | always the case.
        
         | brigadier132 wrote:
         | You don't use them but your favorite libraries do.
        
           | maeil wrote:
           | This is very accurate, in particular when the output type of
           | something depends on the input type.
           | 
           | Data validation, typed database access, or functional
           | programming libraries are good examples. Particularly the
           | modern, leading libraries of such areas, if you look into
           | their code you'll generally see very intricate typing. For FP
           | libraries it's particularly tough. I like to use Remeda which
           | emphasizes being very type-safe, but that means it's
           | inherently more limited in what functions it can offer
           | compare to other libraries which choose to compromise their
           | type-safety. These kinds of techniques mean that libraries
           | can offer greater functionality while remaining type-safe.
        
         | JJMalina wrote:
         | I think the example could be better in this article. Let's say
         | you have a function that takes a database id, an email address,
         | and a name. They're all strings. If you pass arguments to this
         | function and you mess up the order for some reason then the
         | compiler has no idea. Hopefully your unit tests catch this but
         | it won't be as obvious. Branded types solve this problem
         | because you can't pass a name string as an argument that is
         | expected to be an email address.
         | 
         | If you argue that this is not a common problem in practice,
         | then I tend to agree. I haven't seen code with mixed up
         | arguments very much, but when it does happen it can have bad
         | consequences. IMO there is not a big cost to pay to have this
         | extra safety. In TypeScript it's ugly, but in other languages
         | that natively support this typing like Scala, Rust, Swift and
         | Haskell, it works nicely.
        
           | LadyCailin wrote:
           | That's just plain encapsulation, if I understand you
           | correctly. Branding, on the other hand, prevents _complex_
           | types from being confused.
        
             | mason55 wrote:
             | Not really, not for TS at least. If you just want to take a
             | string and call it an email address and have your function
             | only accept email addresses (the simplest use case) then
             | you need to use branding. That's not encapsulation.
        
             | ebolyen wrote:
             | Branding works on primitive types as well, which is I think
             | the most interesting use case.
             | 
             | I would also agree that it's harder to confuse complex
             | types as any single instance of a type is unlikely to
             | overlap once you have a few fields.
        
           | contextnavidad wrote:
           | > because you can't pass a name string as an argument that is
           | expected to be an email address
           | 
           | Unless you accidentally create the wrong branded type? Which
           | is as likely as disordered arguments.
           | 
           | As you stated, tests should cover this case trivially, I
           | don't see the value in added type complexity.
        
         | amatecha wrote:
         | Yeah no, IMO this seems totally extraneous and a layer of
         | complexity not worth introducing to any project. I've never
         | encountered a case where I care that I need to differentiate
         | between two types of exactly the same structure, and my gut
         | feeling (without actually prototyping out an example) is that
         | if this actually makes a difference in your code, you should
         | probably be making the differentiation further up in the flow
         | of information...
        
         | eyelidlessness wrote:
         | Not necessarily wrong. You're probably doing the same work at
         | runtime. That might be either just as good (a subjective
         | preference) or more correct (for certain kinds of dynamism)
         | depending on context. In some cases, there are clear static
         | invariants that _could_ be guaranteed at compile time, and some
         | folks will be more inclined to squeeze as much out of that as
         | possible. In my experience, the benefits of that vary wildly
         | from "total waste of time" to "solves a recurring and expensive
         | problem for good", with a lot in between.
        
       | jonathanlydall wrote:
       | 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.
       | 
       | 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.
        
         | shadowgovt wrote:
         | > as the language is working against you
         | 
         | Right, but this is one of those situations where a reasonable
         | person could conclude "The language is not working to help me
         | solve the problem I'm trying to solve." Sometimes you don't
         | want JavaScript sloppiness and you _also_ don 't want
         | TypeScript structural-type "sloppiness," desiring instead
         | nominal types.
         | 
         | This is a clever way to use the existing infrastructure to get
         | nominal types with no additional runtime overhead.
        
         | kristiandupont wrote:
         | I don't see why. I greatly prefer typescript's structural
         | typing for almost everything. But id's in data models are an
         | exception, so I use branding for those. It works perfectly, the
         | only overhead is in the write-once declaration and now I am
         | protected from accidentally using an AccountId where a MemberId
         | was expected, even though they are both just strings.
        
           | cush wrote:
           | Yes! I've wanted this in Typescript for ages! Is it possible
           | to brand primitives though?
        
             | kristiandupont wrote:
             | Yes you can, it works just the same :-)
        
               | cush wrote:
               | Sorry, with the Symbol? How does it work?
        
               | maskros wrote:
               | You don't even need the symbol. If you want the simplest
               | thing that will work:                   type Velocity =
               | number & { BRAND: "Velocity" }         type Distance =
               | number & { BRAND: "Distance" }              var x = 100
               | as Distance
        
           | amatecha wrote:
           | How do ids of different types accidentally get into a place
           | they shouldn't be? Is this simply a case where someone
           | mistakenly passes along a property that happens to be called
           | "id", not noticing it's an _account_ id rather than a
           | _member_ id (as in, an implementation error)?
        
             | mason55 wrote:
             | Yeah, or what's more likely is that you originally used an
             | AccountID everywhere, but then you decided that you want to
             | use a UserAccountID. So you update your functions but you
             | miss a spot when you're updating all your call sites.
             | 
             | Or you have a client library that you use to interact with
             | your API, and the client library changes, and you don't
             | even notice.
             | 
             | Or you change the return type of a function in a library,
             | and you don't even know who all the callers are, but it
             | sure would be nice if they all get a build error when they
             | update to the latest version of your library.
             | 
             | Lots and lots and lots of ways for this to happen in a
             | medium+ sized project that's been around for more than a
             | few months. It's just another way to leverage the power of
             | types to have the compiler help you write correct code.
             | Most of the time most people don't mess it up, but it sure
             | feels good to know that it's literally impossible to mess
             | up.
        
             | klysm wrote:
             | This kind of mistake is quite easy to make, especially when
             | somebody writes a function that takes several IDs as
             | arguments next to each other. I've seen it happen a number
             | of times over the years
        
             | mejutoco wrote:
             | If you have a distance argument typed as a number, for
             | example, and you pass miles instead of km.
             | 
             | It is equivalent to newtype in haskell.
             | 
             | Another example somebody mentioned here is a logged in
             | user. A function can take a user and we need to always
             | check that the user is logged. Or we could simply create a
             | LoggedUser type and the compiler will complain if we
             | forget.
        
             | sunshowers wrote:
             | A lot of code may end up dealing with multiple kinds of IDs
             | at the same time.
             | 
             | For Rust I wrote newtype-uuid to provide newtype wrappers
             | over UUIDs, which has already found a couple of bugs at
             | Oxide.
             | 
             | [1] https://crates.io/crates/newtype-uuid
        
         | jrockway wrote:
         | Rather, I think Typescript's philosophy of not having a runtime
         | is simply wrong. Other statically typed languages retain type
         | information at runtime. I don't understand why that was so
         | important to explicitly exclude from Typescript.
        
           | nosefurhairdo wrote:
           | If we needed a special Typescript runtime to use Typescript
           | it would become nearly useless. The vast majority of
           | Typescript becomes JavaScript running in Node or V8.
           | 
           | The web is stuck with JavaScript, and Typescript is a tool to
           | help us write maintainable JavaScript.
        
             | williamcotton wrote:
             | Could't one write a TypeScript runtime in JavaScript or
             | WASM?
        
               | kevingadd wrote:
               | It wouldn't be very useful, since a TS-in-JS runtime
               | would have significant performance overhead, and a TS-in-
               | WASM runtime would have very expensive JS interop plus
               | cross-language-GC hurdles. Might be less bad with WASM
               | GC.
        
               | DonHopkins wrote:
               | Does branding work in AssemblyScript?
        
               | moritzwarhier wrote:
               | With horrible overhead, and in the case of WASM, lots of
               | serialization and API glueing (DOM is just somewhere near
               | the tip of the iceberg) woes, maybe?
               | 
               | Would be a fun thing to do for sure, but never as fast as
               | the APIs built into the browser runtime.
        
           | recursive wrote:
           | _Static_ typing information isn 't retained at runtime. e.g.
           | ListInterface list = new ConcreteList();         let
           | runtimeType = GetTypeOf(list); // will be `ConcreteList`, not
           | `ListInterface`
           | 
           | This is an imaginary java-like language, but I'm not aware of
           | a statically typed language that gives you the static type
           | resolutions at run-time, outside of cases like implicit
           | generic resolutions and things like that.
        
           | vundercind wrote:
           | So it's Just JavaScript and the risk of adopting it is
           | basically zero.
           | 
           | It's _the_ thing that made it an easy sell after everyone got
           | turned off by the long-term experience of Coffeescript and
           | such. It's _the_ reason various "better" typed languages that
           | run on top of JS have flopped except with enthusiasts.
        
           | mason55 wrote:
           | The decision between structural and nominal typing has
           | nothing to do with whether you retain type information at
           | runtime.
           | 
           | TS could have just as easily chosen nominal typing + a simple
           | way to do typedefs and had everything else work like it does
           | now. But structural typing gives you a lot of _other_ useful
           | features.
        
           | klysm wrote:
           | I disagree. If we had a runtime we'd have to inject that all
           | over the place and compatibility with javascript wouldn't be
           | a given.
        
           | Quekid5 wrote:
           | > Other statically typed languages retain type information at
           | runtime.
           | 
           | Funnily enough, Haskell[0] doesn't... unless you ask it to by
           | explicitly asking for it via Typable[1].
           | 
           | [0] ... which is renowned/infamous for its extremely
           | static+strong typing discipline. It _is_ nice that one can
           | opt in via Typeable, but it 's very rare to _actually_ need
           | it.
           | 
           | [1] https://hackage.haskell.org/package/base-4.19.1.0/docs/Da
           | ta-...
        
           | hpeter wrote:
           | Typescript is not a real standalone language. It's just
           | javascript after all.
           | 
           | Try Dart, maybe you like it. It's like TS but with it's own
           | runtime.
        
       | JJMalina wrote:
       | Zod has branded types: https://zod.dev/?id=brand it works really
       | nicely with Zod schemas
        
       | spankalee wrote:
       | 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.
       | 
       | 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.
       | 
       | I use branding enough that I have a little helper for it:
       | /**          * 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;         };
       | 
       | Use it like:                   type ObjectId = Brand<string,
       | 'ObjectId'>;
       | 
       | And the hover-over type is:                   type ObjectId =
       | string & {           readonly __ObjectId_brand: never;         }
        
         | cush wrote:
         | Sorry, how do you apply it to branding primitives? The basic
         | const accountId: Brand<string, "Account"> = "123654"
         | 
         | Has the error                   Type 'string' is not assignable
         | to type '{ readonly __Account_brand: never; }'
        
           | beeboobaa3 wrote:
           | This error is, in fact, the point. It keeps you from
           | accidentally assigning a normal string to a branded string
           | 
           | You have to make a function to apply the brand via a cast,
           | the article explains this as well.                   function
           | makeObjectId(id: string): ObjectId {             return id as
           | ObjectId;         }
        
             | cush wrote:
             | Ah, yeah the error makes sense. I expected the error, just
             | wanted to understand how Brand was meant to be actually
             | assigned to a primitive. I'm not sure the function is
             | necessary though. This does the same thing
             | const accountId = "125314" as AccountId
             | 
             | It makes sense that the technique uses casting.
        
               | spankalee wrote:
               | The function is great in cases where you can validate the
               | string, or paired with lint rules that limit casting.
        
         | amatecha wrote:
         | Yeah in TS' own playground example they don't create a Symbol,
         | they just intersect it inline:
         | https://www.typescriptlang.org/play#example/nominal-typing
        
           | spankalee wrote:
           | Nice.
           | 
           | I don't love that example though, because the brand field's
           | value of a string literal will make it seem like the object
           | actually has a property named `__brand` when it doesn't.
           | `never` is the best brand field type as far as I can tell.
        
             | wk_end wrote:
             | `never` is a problematic field type, at least unless you
             | make efforts (via non-exported symbols, for instance) to
             | make sure the field is inaccessible - `never` is the type
             | of a value that doesn't exist; it's there in the type
             | system to signify an impossible scenario. For instance, a
             | common use-case is to mark the type of a variable after
             | type narrowing has exhausted every possible case.
             | 
             | If you assert that your id's actually _do_ have a never-
             | typed field, and you let your program access it, you 're
             | basically letting users enter an impossible state freely
             | and easily.
        
               | spankalee wrote:
               | Isn't that what you want to signify? It's the intent, and
               | better than asserting that your IDs have a string-valued
               | field that it doesn't.
               | 
               | Ideally you could brand with a private field, but we
               | would probably need `typeof class` for that (assuming
               | `typeof class` allows private members. I'm not sure).
        
               | wk_end wrote:
               | From the direction of construction, it is - as a marker
               | of "I want to never be able to construct this value - the
               | only way I should be able to construct this value is in
               | an impossible state", sure, it works.
               | 
               | But from the direction of usage...because you've used
               | casting to (as far as TypeScript is concerned) construct
               | the value, once it's floating around you're in an
               | impossible state - and no, _having_ a branded thing
               | should not be an impossible state. Because of that you
               | can freely violate the principles of the type system 's
               | logic - ex falso quodlibet.
               | 
               | A never value is effectively an any value, and now you
               | have one on hand at all times.
               | 
               | https://www.typescriptlang.org/play?#code/FAMwrgdgxgLglge
               | wgA...
        
         | nsonha wrote:
         | do you need readonly with never?
        
       | leecommamichael wrote:
       | 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.
        
       | techn00 wrote:
       | Very useful for DDD, like having an Email type, or String100
       | (string of 100 characters)
        
         | mason55 wrote:
         | Works very well too for any kind of validation or encoding.
         | Anything that accepts input from the outside world can accept a
         | string. And then everything else in the app can work with a
         | "SafeString" and the only way to create a safe string is to
         | send a string through a string escape function (or whatever
         | makes sense for your app).
         | 
         | Works especially well if you're using any kind of hexagonal
         | architecture, make your functional core only accept
         | validated/escaped/parsed/whatever types, and then the
         | imperative shell _must_ send any incoming data through whatever
         | transformation /validation/etc before it can interact with the
         | core.
        
       | zbentley wrote:
       | 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.
       | 
       | 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.
        
         | shadowgovt wrote:
         | This is one of those frustrations I encounter with C++.
         | 
         | C++'s templating lets express some very powerful type
         | constraints... But good luck reading the compiler errors
         | generated by someone _else 's_ very powerful type constraints
         | that you haven't fully grokked to an implementation level.
        
         | eropple wrote:
         | Generally speaking, I wouldn't use branded inputs for
         | _libraries_. Branded types make a lot more sense to me when
         | working in business-logic cases, to identify data at the edge
         | of a bounded context and tracing through the system. A library
         | is downstream of that and the code that requires the branded
         | type should be controlling the inputs to the library.
        
         | eyelidlessness wrote:
         | I've found TypeScript errors can be made a lot easier to
         | understand with some forethought about how invalid types are
         | defined in the first place. For instance, a many-branched
         | conditional type may be much easier to understand (both as a
         | library user and as a reviewer/maintainer of the library code
         | itself) if each `never` (or whatever actual failure case) is
         | instead a string literal describing the nature of the failure.
         | And better still if it can be templated with an aspect of the
         | input type to highlight what aspect of it triggered the error.
        
       | jibbit wrote:
       | or a tagged type if you're old enough to have ever been exposed
       | to another language
        
       | mirekrusin wrote:
       | Flow is much better with opaque types.
       | 
       | Also nominal types for classes.
       | 
       | And correct variance.
       | 
       | And adhering to liskov substitution principles.
       | 
       | And exact object types.
       | 
       | And spread on types matching runtime behavior.
       | 
       | And proper no transpilation mode with full access to the
       | language.
       | 
       | And has 10x less LoC than ts.
       | 
       | ps. before somebody says "flow is dead" have a look at flow
       | contributions [0] vs typescript contributions [1]
       | 
       | [0] https://github.com/facebook/flow/graphs/contributors
       | 
       | [1] https://github.com/microsoft/TypeScript/graphs/contributors
        
         | eyelidlessness wrote:
         | FWIW, classes in TypeScript become nominal types when they have
         | a non-public member. But I definitely do feel real FOMO over
         | Flow's opaque types.
        
           | mirekrusin wrote:
           | Yes, ts is full of this kind kind of adhoc-ifs-like glued
           | together, also exactness check only when it's literal object
           | etc.
           | 
           | Flow is more principled.
        
             | eyelidlessness wrote:
             | I mean. I think this specific case is a lot more principled
             | than you seem to think. It's certainly well reasoned from a
             | perspective of structural typing by default.
        
               | mirekrusin wrote:
               | Yes, they have their reasons. They always do, tradeoff
               | etc, I know.
               | 
               | It doesn't change the fact that ie. adding private member
               | is breaking change in your library which is kind of funny
               | (until it's not funny of course).
               | 
               | Also stuff like:                   class Foo { private
               | foo = 1 }         class Bar {}         const a: Bar = new
               | Foo
               | 
               | ...typechecks so that's it for nominality.
               | 
               | It's all ifs all the way down.
        
               | eyelidlessness wrote:
               | It's only a breaking change if you've been using a class
               | to stand in for an interface (happens to the best of us
               | I'm sure!). You can still avoid it being one after the
               | fact by introducing a non-breaking interface consistent
               | with what you were already accepting/expecting.
               | 
               | And yeah, I'm not a fan of that class instance
               | assignability case. Not to make excuses for it, but I
               | have other reasons I generally prefer to expose
               | interfaces (distinct from classes, even if they're
               | identical in shape and even have the same internal
               | purpose) at most API boundaries; avoiding that particular
               | footgun just turns out to be a nice happy side effect of
               | the preference.
        
         | nsonha wrote:
         | > adhering to liskov substitution principles
         | 
         | what does this even mean?
         | 
         | > And has 10x less LoC than ts
         | 
         | Prolly b/c Flow isn't able to express the advanced types
         | (albeit with 10x LoC) in the first place.
        
           | mirekrusin wrote:
           | You can google, gpt or look at wikipedia for "liskov
           | substitution principles". It's related to object oriented
           | programming, more specifically to inheritance and what is
           | allowed as substitution as superclass vs subclass depending
           | on which position it sits in argument vs return value. It's
           | very interesting read if you don't know about it and you're
           | using OOP.
           | 
           | What advanced types do you have in mind?
           | 
           | ps. the way you're using "albeit" sounds like you think flow
           | has 10x larger codebase, it has 10x smaller codebase
        
         | recursive wrote:
         | Check out the react codebase, which is presumably the flagship
         | use of flow. It has hundreds of FLOW FIXME annotations. It's
         | easier have a lot of features and small code base if the you
         | don't handle the difficult cases.
        
         | vundercind wrote:
         | Tried Flow first, back in the day. I was ready to give up the
         | whole idea as not-at-all-worth-the-trouble if Typescript had
         | been as mediocre an experience.
         | 
         | Fortunately it wasn't and now I get to not-hate working in
         | JavaScript.
        
           | mirekrusin wrote:
           | Yes there was a time when atom/vscode integration was shit.
           | 
           | They also fucked up typings in terms of community management.
           | 
           | Those two alone probably put them into downward spiral.
           | 
           | But the language is being developed with activity stronger
           | than ever.
           | 
           | And it is well designed and pleasure to code in.
        
         | baw-bag wrote:
         | flow is dead (for jobs). I'm gonna annihilate Dart (apart from
         | jobs)
        
           | mirekrusin wrote:
           | java is great for jobs as well.
        
       | klysm wrote:
       | The downside of structural typing
        
       | dimitrisnl wrote:
       | Selfish plug about the same topic
       | https://dnlytras.com/blog/nominal-types
        
       | dvt wrote:
       | Weird idea, as types in TS are structural by _design_. If this is
       | something you need, it smells like  "runtime checking" not
       | amending the type system.
        
         | williamdclt wrote:
         | Design doesn't cover all use cases. There's nothing weird about
         | wanting different types for "user id" and "organisation id" so
         | that you don't use the wrong argument by mistake
        
           | dvt wrote:
           | While you're right that branding makes passing arguments more
           | ergonomic, I will still always be able to do `as OrgId` or
           | `as UserId` so you need to do have _some_ failure handling
           | anyway, unless you 're okay with blowing up in the user's
           | face.
        
             | strongly-typed wrote:
             | The way you're describing it sounds to me like you are
             | adding failure handling to handle cases where the
             | programmer is intentionally misusing the thing. I would
             | argue that in this type of situation, the error handling is
             | not necessary because it would hide the fact that the thing
             | is being misused.
             | 
             | Or perhaps I'm misunderstanding your comment. When you do
             | `as OrgId` or `as UserId`, where do you envision those
             | casts in ways that would require handling failures?
        
               | dvt wrote:
               | My point is that the API consumer does not guarantee
               | types (say it's REST or something), so the assumption
               | that the string you send it will always be the right type
               | (or call it "format") seems like a bad one. Unless you
               | control API usage from end-to-end (which kind of defeats
               | the point of an API), you need error checking (or at
               | least exceptions to bubble up).
               | 
               | A lot of times branding is used to mark data received
               | from a database, e.g.: this field is an `OrgId`, but I
               | can do all kinds of things to that string which might
               | make it not-an-`OrgId` at any point. Then, I'll try to
               | reuse it the `OrgId`, and I'll get some weird error or
               | blowup and I'll have no idea why. So the point is that
               | (a) branding is a dubious feature because it can
               | obfuscate soft-type-breakage (I call it "soft" because at
               | the end of the day, we're just dealing with strings), and
               | (b) it still doesn't preclude runtime error checking
               | unless you're okay with blowups.
        
               | dbalatero wrote:
               | > Unless you control API usage from end-to-end (which
               | kind of defeats the point of an API)
               | 
               | Isn't this all frontend client bundles that talk to their
               | own private backend API? Those are controlled end-to-end.
               | My company has one, yours probably does too!
        
             | williamdclt wrote:
             | That's nothing specific to branded types. We know
             | Typescript doesn't enforce anything at runtime, whether
             | it's primitive types, complex types, nullability or
             | anything else, that's nothing new.
             | 
             | It's not about security, it's about safety: make it harder
             | to do the wrong thing, and make it easier to do the right
             | thing than the wrong one.
        
         | RHSeeger wrote:
         | Right, but there are times when structural typing is not the
         | right choice. And runtime checking is ... suboptimal. I mean,
         | saying runtime checking is the right choice is like saying a
         | type system isn't necessary, because you have automated tests.
         | They're great, and they're a tool that provides value... but so
         | too are types.
        
           | dbalatero wrote:
           | > And runtime checking is ... suboptimal.
           | 
           | Especially on the frontend, where your `throw new Error("Bad
           | input type")` might brick the entire app if uncaught. I'd
           | much rather hear an earful from TypeScript before a bundle is
           | ever produced.
        
         | tshaddox wrote:
         | > If this is something you need, it smells like "runtime
         | checking" not amending the type system.
         | 
         | This is both! The primary point of branded types is to allow
         | you to use the type system to ensure that a particular runtime
         | check has taken place.
        
           | dvt wrote:
           | > The primary point of branded types is to allow you to use
           | the type system to ensure that a particular runtime check has
           | taken place.
           | 
           | I see. This is kind of cool, though the branding can still be
           | broken via down-the-stream mutations. Would be nice to
           | enforce re-branding every time a variable is changed, but
           | that seems like a lot of overhead.
        
             | tshaddox wrote:
             | > the branding can still be broken via down-the-stream
             | mutations
             | 
             | Only for mutable values! A branded string should be as
             | immutable as they come, right?
        
       | erik_seaberg wrote:
       | 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.
       | 
       | 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).
        
         | diarrhea wrote:
         | As a TypeScript beginner, I was bitten by this. Typing in
         | TypeScript feels quite bad for some reason; a lot of effort for
         | effects which are still potentially wrong at runtime. I didn't
         | struggle so much in Python, Rust or C#, for example. Python is
         | surprisingly... sound? in comparison. It can do nominal as well
         | as structural typing.
        
       | beders wrote:
       | Ah, the magical disappearing type system - now being used for
       | nominal typing.
       | 
       | I'm curious to see what the JS code looks like for casts and type
       | checks in that case.
        
         | williamdclt wrote:
         | Nothing special about casts and type checks. It's just a
         | simpleish workaround for when nominal typing is more useful
        
         | klysm wrote:
         | > Ah, the magical disappearing type system - now being used for
         | nominal typing.
         | 
         | More like: magical disappearing type system is not nominal:
         | hacky workarounds ensue.
        
       | Slix wrote:
       | This is also useful for having a type that needs to be verified
       | in some way before being used or trusted. UnverifiedLoginCookie
       | vs. VerifiedLoginCookie
        
       | lang_agnostic wrote:
       | Isn't this just phantom types in other programming languages?
        
         | valcron1000 wrote:
         | More like `newtype` with implicit `coerce`
        
       | thepaulmcbride wrote:
       | Anytime I've come across the need to do this, I've found a class
       | is a better and less complicated solution.
       | 
       | I really like the pattern of value objects from Domain Driven
       | Design. Create a class that stores the value, for example email
       | address.
       | 
       | 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.
       | 
       | As far as I understand classes are the only real way to get
       | nominal typing in TypeScript.
        
         | CharlieDigital wrote:
         | Classes can also make use of the native `instanceof` JavaScript
         | operator [0].
         | 
         | It is also possible to then infer a type from a class so you
         | can use both the class where you want to discriminate types and
         | the type where you really only care about the shape.
         | 
         | The absolutism towards OOP/FP -- instead of embracing the right
         | use cases for each -- always ruffles me in the wrong way. C#,
         | for example, does a great job of blending both OOP and FP
         | (borrowing heavily from F# over the years). JS and by extension
         | TS has the same flexibility to use the right paradigm for the
         | right use cases, but it seems that everyone wants to be on one
         | end of the spectrum or the other instead of accepting that JS
         | is an amalgamation.
         | 
         | Evan You had a great quote on this where he correctly calls out
         | much of the complexity and performance issues with React as
         | being rooted in pushing against the language rather than
         | embracing it.
         | 
         | [0] https://developer.mozilla.org/en-
         | US/docs/Web/JavaScript/Refe...
        
           | thepaulmcbride wrote:
           | Classes are very underutilised in TypeScript. I recently
           | introduced them to our codebase at my day job and got a fair
           | bit of pushback because it wasn't "JavaScripty" enough.
        
             | tadfisher wrote:
             | That's a weird objection, because Typescript classes are
             | literally Javascript classes[1].
             | 
             | [1]: https://developer.mozilla.org/en-
             | US/docs/Web/JavaScript/Refe...
        
         | spankalee wrote:
         | Yeah, classes are generally better than branded types for
         | objects (unless you're just building discriminated unions).
         | 
         | What's particularly better these days is that you get nominal
         | typing and brand checks with standard private fields:
         | class Foo {           #brand;                static isFoo(o): o
         | is Foo {             return #brand in o;           }         }
        
         | tshaddox wrote:
         | > As far as I understand classes are the only real way to get
         | nominal typing in TypeScript.
         | 
         | Although classes and instances are ultimately structural,
         | right?                   let foo = new Foo();
         | assert(foo.constructor == Foo);
        
         | theteapot wrote:
         | > Anytime I've come across the need to do this, I've found a
         | class is a better and less complicated solution ... As far as I
         | understand classes are the only real way to get nominal typing
         | in TypeScript.
         | 
         | How are classes going to help? As far as I understand TS is
         | structural period. Ex this is valid (!):                 class
         | Email {           constructor(public email: String) {}       }
         | let x: Email = new Email('foo@foo.com')       let y = { email:
         | '73'}       x = y;
        
       | bazoom42 wrote:
       | I think Typescript need nominal type aliases for primitives.
        
       | satvikpendem wrote:
       | Reminds me of another sort of type-driven development, making
       | invalid states unrepresentable: https://geeklaunch.io/blog/make-
       | invalid-states-unrepresentab...
        
       | herpdyderp wrote:
       | 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>`
        
       | munk-a wrote:
       | 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.
       | 
       | 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.
        
       ___________________________________________________________________
       (page generated 2024-04-24 23:00 UTC)