spankalee 10 days ago

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 10 days ago

    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 10 days ago

      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 10 days ago

        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 10 days ago

          The function is great in cases where you can validate the string, or paired with lint rules that limit casting.

          • cush 10 days ago

            At that point I'd just use a class constructor.

            • spankalee 10 days ago

              That has a lot of overhead compared to validating and casting a primitive.

              • cush 9 days ago

                Does it though?

        • yencabulator 9 days ago

          And herein lies one of the worst features of Typescript.

              const myCompanyId = something_complicated as CompanyId;
          
          where something_complicated actually is a UserId, not a bare string.

          It is way too easy to accidentally destroy type safety with `as`, there are absolutely 0 safeguards.

          I fear every single instance of `as` in any Typescript source I see.

        • galaxyLogic 10 days ago

          That's a very clear example.

          But what if I instead wrote:

             const accountId = AccountId ("125314" );
          
          There would be the needed checks and balances in the function AccountId(). Wouldn't that do pretty much the same thing?
          • svieira 10 days ago

            Yes, but then there would be runtime overhead for the wrapper (presuming a class or object is returned) or the type of accountId would be the underlying type (if AccountId isn't returning a branded type).

          • cush 10 days ago

            I personally prefer less code and more explicit casting

            • whilenot-dev 10 days ago

                const accountId = "125314" as AccountId;
              
              or

                const accountId = new AccountId("125314");
              
              or

                const accountId = accountIdFromString("125314");
              
              is all "explicit" casting in my understanding, one way or the other, and the first ... as ... being on the lowest level.

              I'd rather go for something like "do the casting close to the source, and in general use 'parse, don’t validate'[0] when getting input from the outer world".

              [0]: https://lexi-lambda.github.io/blog/2019/11/05/parse-don-t-va...

              • usrusr 9 days ago

                The as also does not burden the reader with guessing about the absence of unexpected stuff happening in the functions. Don't pepper the code with mostly unused hidey-holes just to perhaps save some busywork later.

                If you do add valuation, make it an honest asValidatedAccountId("123"), not an accountIdFromString("123") that may or may not do more than casting.

                (PS, very much off topic: speaking of hidey-holes, are any of the AOP monstrosities still operational?)

              • cush 9 days ago

                Agree. I was talking about

                    const accountId = AccountId (“43525”)
                
                being less explicit
      • epolanski 10 days ago

        Which, in real life, makes you parse (and handle or throw errors or log, whatever you want) but then now that the item you have is what you need.

        Another way you can implement the same: through a class constructor like new UserId('some string') which can also throw or let you handle operations on the class itself while allowing you to get the value.

  • amatecha 10 days ago

    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 10 days ago

      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 10 days ago

        `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 10 days ago

          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 10 days ago

            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/FAMwrgdgxgLglgewgA...

          • eyelidlessness 10 days ago

            The real problem with `never`, or at least the problem that could actually affect you in this scenario, is that it’s assignable to everything. Just like `any`, but even strict lint rules usually won’t catch the type safety it escapes.

            If by some mistake your code gets a reference to a field with a `never` type, you’ll never get a type error passing or assigning that reference downstream. That’s relatively trivial to address if you can spot the (edit: runtime) errors it causes directly, but can wreak terrible havoc if it finds a path through several layers of function calls or whatever other indirection before the runtime consequences surface.

          • lmm 10 days ago

            > Isn't that what you want to signify? It's the intent

            No it isn't. If your ids do have a field, then marking them as never having a field is unwise. Never means that code that reads that field is never wrong (because you can never reach the point of reading it), whereas you want the opposite, code that reads that field is always wrong.

  • nsonha 10 days ago

    do you need readonly with never?

    • samtheprogram 10 days ago

      Yes, otherwise consumers could try reading the property (that doesn’t exist).

      • nsonha 10 days ago

        how would readonly fixes consumers reading the property?

        • whilenot-dev 10 days ago

          it doesn't. never prevents reading and readonly prevents (over)writing.

          GP just focused on the never-part of your question.

          • nsonha 9 days ago

            never means nothing can be assigned to it, so in that sense it's already readonly

            • samtheprogram 7 days ago

              Correct, so the question should have been: is readonly necessary?

              I wouldn’t call myself an expert in TypeScript’s type system (although I use it daily), so I’m not sure about that one.

              I do know I would have omitted readonly if I did this myself, but perhaps by mistake.

  • 38 10 days ago

    Is all Typescript unreadable, or is that just your style?

    • spankalee 10 days ago

      Are all of your comments devoid of content, or just this one?

      • 38 9 days ago

        Are all of your comments devoid of content, or just this one?

    • highwaylights 10 days ago

      It’s very readable to me, and is much cleaner than the notation in the article.

      Is your problem the line wraps in the parent’s comment?

      • 38 9 days ago

        this is readable?

            export type Brand<T, Brand extends string> = T & {
              readonly [B in Brand as `__${B}_brand`]: never;
            };
        
        I count 14 different pieces of punctuation. you might as well use Perl at that point.
        • scotty79 9 days ago

          It's very readable if you know what every bit means.

              export type Brand<T, Brand extends string> = T & {
                readonly [B in Brand as `__${B}_brand`]: never;
              };
          
          It exports a freshly defined type named Brand, which is a generic type with two parameters, one named T (that can be any type), and other named Brand (probably should be named differently to avoid confusion with the name of the whole exported type). Brand parameter must be a type that's assignable to string. For example it can be a type that has only one specific string like "Account" allowed as a value. It might be a bit weird but in places where type is expected "Account" is not treated as string literal but as a type assignable to string with only one allowed value, "Account".

          This whole exported type is defined as an intersection type of parameter T and object type that has readonly properties for every type that is assignable to the type passed in as parameter named Brand when this generic type is instantiated. For example if "Account" was passed then B can only be "Account" here, so this object type will only contain a single property. Key of this property will be a string that looks like __Account_brand (if the Brand was "Account") and the type of this property will be never, so nothing is allowed to be assigned to this property.

          The result of instantiation of this whole exported generic type will be a type which values can be assigned to variables of type T, but attempt to assign value of type T (or type branded with some other string) to variable of this branded type will result in an error because of missing or mismatched brand property.

          This definition might allow for interesting complex branding like:

              let id:Brand<string, "Employee" | "Manager">;
          
          that can be assigned to variables and parameters of types Brand<string, "Employee"> or Brand<string, "Manager"> or just string.

          PS.

          Intersection type constructed from multiple types in TS using & symbol is a type which values are assignable to variables of each of the constituent types.

          For example value of a type Logger & Printer can be assigned to variables and parameters of type Logger and of type Printer as well.

          • 38 9 days ago

            that is WAY too complicated. not to mention using enough of that probably destroyed compile time/interpretation time. every time I see "advanced type logic" it seems to ruin a language, because people dont know how to use it in moderation.

            • scotty79 8 days ago

              I can't really argue with that. It's a way to get nominal types in a language that has structural typing. Even though the usage is simple, the implementation is more complicated than it should be.

              If for example the language was missing builtin hashmap type, its implementation would be nasty as well.

              I'm not a huge fan of typing systems, the thing I like the most in TypeScript is that you can use types as little as you want and add more only when you want it. But I don't like languages like go that intentionally lack features.

              • 38 8 days ago

                > languages like go that intentionally lack features

                people will go as far down the rabbit hole as you let them, which is what the Go developers understand and are trying to account for:

                https://www.hyrumslaw.com

                • scotty79 8 days ago

                  I understand it's merit. I just don' like it. I think it's at least 100 years too soon for that. Exploration is important.

        • gmdrd 7 days ago

          Equivalent code in Python:

            from typing import NewType
        • mock-possum 9 days ago

          I mean you’re not wrong, this is getting pretty far into the weeds in terms of typescript syntax.

thepaulmcbride 10 days ago

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 10 days ago

    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 10 days ago

      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 10 days ago

        That's a weird objection, because Typescript classes are literally Javascript classes[1].

        [1]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Refe...

        • afavour 10 days ago

          There was plenty of outrage when classes were added to JS. “JS uses prototypal inheritance, not OOP!”

          • krapp 10 days ago

            I'm still a bit cranky about that, but because objects should be good enough for anyone. They aren't even really classes anyway.

            • Aeolun 10 days ago

              If it walks like a duck, and quacks like a duck, for all intents and purposes, it is a duck.

      • chrysoprace 10 days ago

        I like classes well enough; I just find that often they over-complicate simple code, especially when you introduce inheritance. Since classes are effectively just syntax sugar I often find it's more intuitive to go with object syntax but to each their own. So long functionality is isolated in modules I'm happy.

    • eyelidlessness 9 days ago

      > The absolutism towards OOP/FP

      … also doesn’t need to proscribe use or non-use of classes. People were doing OOP in JS long before classes were added to the language. I routinely use classes in a FP style because they’re excellent for modeling value types with a stable structure.

    • schwartzworld 9 days ago

      JS classes are a great fit for FP. They're just functions with closures and a little syntactic sugar.

  • svieira 10 days ago

    Given a Branded / Nominal interface, they look about the same usage wise but the branded type disappears at runtime (whether that is a benefit or a detriment depends on your use case):

        type Email = Branded<string, 'email'>
    
        function Email(maybeEmail: string): Email {
          assertIsEmail(maybeEmail);
          return maybeEmail;
        }
    
        function assertIsEmail(value: string): asserts value is Email {
          if(!isEmail(value)) throw TypeError("Not an email");
          return value;
        }
    
        function isEmail(value: string): value is Email {
          return value.contains("@");
        }
    
    Versus:

        class Email {
          #email: string;
          public constructor(maybeEmail: string) {
            if(!isEmail(maybeEmail)) throw TypeError("Not an email");
            this.#email = maybeEmail;
          }
          valueOf() {
            return this.#email;
          }
        }
  • tshaddox 10 days ago

    > 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);
    • chrismorgan 9 days ago

      foo.constructor actually doesn’t matter; foo.__proto__ == Foo.prototype is the one that actually makes things chug, and you can change the class of a value by changing its __proto__.

  • theteapot 10 days ago

    > 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;
    • superamadeus 10 days ago

      I had the same initial reaction, but it turns out that if you have at least one private member, even with the same signature, it works as the parent comment suggests.

      For example, these two classes are distinct to TypeScript:

          class Name {
              constructor(private value: string) {}
      
              getValue() {
                  return this.value;
              }
          }
      
          class Email {
              constructor(private value: string) {}
      
              getValue() {
                  return this.value;
              }
          }
      
          const email: Email = new Name(“Tim”); // Error: (paraphrasing) conflicting private declaration of “value”.
      • thepaulmcbride 10 days ago

        You’re right. This is what I meant, but I was not at all clear in my original comment.

    • DanielHB 9 days ago

      I believe what the OP meant by "to get nominal typing in TypeScript" is that the type information is available at runtime through the use of the `instanceof` keyword.

      I am surprised your code doesn't give an error, x and y are definitely not the same thing because `x instanceof Email` would be false at the end of that code. But like you said, to TS x and y are indeed the same type because they have the same structure. In practice both can be used interchangeably in the code (even if Email extended another class) with the sole exception of the `instanceof` keyword.

  • spankalee 10 days ago

    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;
          }
        }
    • iainmerrick 9 days ago

      I don’t quite see why you’d need to brand simple objects at all! Just give them meaningful field names.

      Branding primitives is useful because you don’t otherwise have a name that you can control (e.g. feet versus meters as was suggested elsewhere in this thread, both of which would have type “number”).

      The article starts with the example of two different object types with field “x: number”. In that situation you should be branding the field, not the object! If the field has the same name and the same branded type, structural typing is correct for that object shape.

    • mbwgh 9 days ago

      Is this different/better than using `instanceof`?

    • superamadeus 10 days ago

      This is really neat, thank you for sharing!

epolanski 10 days ago

An example of what you can implement on top of branded types that I want to share with fellow hackers:

- currencies

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).

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.

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 easier to be handled with stuff like dictionaries or tagged unions.

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:

https://github.com/gcanti/money-ts

  • spankalee 10 days ago

    As much as I love branding, I would be hesitant to use it for critical math and where you might want to inspect the type at runtime as with money.

    Branding is easily circumvented, so it's best as a developer hint that helps document APIs and alerts us to common mistakes as an incremental improvement over primitives.

    For money and similar I would use objects and a custom math library.

    • epolanski 9 days ago

      You can parse before brand thus enhance and enforce runtime correctness.

  • atonse 10 days ago

    Thanks, the currency stuff was a great real-world example that crystallized where this would be nice to have at a core level with typescript to severely reduce the odds of doing cross-currency math.

  • robocat 9 days ago

    I have seen plenty of currencies come and go in my life: weird to lock that into your codebase.

    And I'm struggling to think of where in a codebase I might want to hardcode Pesos or Euros.

jonathanlydall 10 days ago

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.

  • kristiandupont 10 days ago

    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.

    • amatecha 10 days ago

      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)?

      • klysm 10 days ago

        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

        • z3t4 9 days ago

              function foo(customerId, itemId, orderId) {
                  if(!customer[customerId]) throw new Error("Customer with id=" + customerId + " does not exist");
                  if(!item[itemId]) throw new Error("Item with id=" + itemId + " does not exist");
                  if(!order[orderId]) throw new Error("Order with id=" + orderId+ " does not exist");
              }
          
          Just an example of how you can detect errors early with defensive programming.
          • antifa 9 days ago

            As oppose to a zero cost build time check, that's 3 expensive runtime DB checks, plus an account and a member could have the same ID, so it might run anyways but on the wrong DB rows.

            • z3t4 7 days ago

              You made two points that are correct in theory, but in practice those lookups would/could take micro-seconds, and the bug would be detected within minutes in a production environment. And it would take five minutes or less to patch.

              • antifa 6 days ago

                If you're cowboy coding as a solo dev, sure.

                • z3t4 2 days ago

                  We could argue if having 1-3 code cowboys is better then a large team. If the code cowboys can produce higher quality, quicker. They will be difficult to replace after many years though.

          • klysm 9 days ago

            Yeah this is exactly why using primitive types for ids is bad. If you encode it in the type system then you cannot make this error and don't need to check it at runtime.

            • z3t4 7 days ago

              Most bugs are found at runtime. The static analysis mostly finds silly errors that are easy to fix. A great type system do make it easier to write spaghetti code though, and you can have one letter variable names - naming is difficult.

      • sunshowers 10 days ago

        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

      • mason55 10 days ago

        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.

      • mejutoco 10 days ago

        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.

      • sophiabits 9 days ago

        Happens a lot with junction tables ime. e.g. At my last job we had three tables: user, stream, user_stream. user_stream is an N:N junction between a user and a stream

        A user is free to leave and rejoin a stream, and we want to retain old data. So each user_stream has columns id, user_id, stream_id (+ others)

        Issues occur when people write code like the following:

        streamsService.search({ withIds: userStreams.map((stream) => stream.id), });

        The issue is easily noticed if you name the “stream” parameter “userStream” instead, but this particular footgun came up _all_ the time in code review; and it also occurred with other junction tables as well. Branded types on the various id fields completely solve this mistake at design time.

    • cush 10 days ago

      Yes! I've wanted this in Typescript for ages! Is it possible to brand primitives though?

      • kristiandupont 10 days ago

        Yes you can, it works just the same :-)

        • cush 10 days ago

          Sorry, with the Symbol? How does it work?

          • maskros 10 days ago

            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
  • shadowgovt 10 days ago

    > 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.

  • jrockway 10 days ago

    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 10 days ago

      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 10 days ago

        Could’t one write a TypeScript runtime in JavaScript or WASM?

        • DanielHB 9 days ago

          Using any JIT language (most implementations of python, Java, ruby, JS, etc) in WASM means porting the runtime to WASM and then having the runtime (in WASM) parse and JIT-compile your code incurring a double-whammy of performance degradation (WASM runtime compiling your JIT language runtime and then your JIT language runtime compiling your actual code).

          To do this kind of thing viable there are two ways I can think of:

          1) proper ahead-of-time compiler for your language targeting WASM directly. But that means pretty much rebuilding the whole stack for the language as that is a completely different approach.

          2) Do something like Android Runtime (ART) does which translates Java bytecode to native machine instructions whenever you install an android app. But in this case translate the bytecode to WASM and do it before distribution. This requires a new compiler-backend which is still quite complex.

          Both of these mean you don't have a language runtime at all. There is a reason most WASM stuff you see is in written C/C++/Rust and it is not just the lack of GC.

        • kevingadd 10 days ago

          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 10 days ago

            Does branding work in AssemblyScript?

        • moritzwarhier 10 days ago

          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.

    • vundercind 10 days ago

      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.

    • recursive 10 days ago

      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.
    • Quekid5 10 days ago

      > 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/Data-...

    • klysm 10 days ago

      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.

    • mason55 10 days ago

      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.

    • hpeter 10 days ago

      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.

munk-a 10 days ago

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.

  • cageface 10 days ago

    The other problem I've found with structural types in Typescript is that it can lead to some very complex type specifications. Instead of carefully thinking about and naming the entities in your system you can get some very hard to parse type spaghetti.

  • skybrian 10 days ago

    I haven’t run into interfaces coincidentally matching in Go. Have you? It might happen more easily for primitive types, though, and Go does have a way to declare new types:

       type UserId string
  • Aeolun 10 days ago

    I mean, they have to stay backwards compatible with Javascript. That kind of limits your options.

    • munk-a 9 days ago

      Not particularly - they could use distinct tokens (i.e. these brandings) on all types implicitly.

dvt 10 days ago

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 10 days ago

    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 10 days ago

      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.

      • williamdclt 10 days ago

        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.

      • strongly-typed 10 days ago

        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 10 days ago

          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.

          • Dylan16807 10 days ago

            > 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.

            The raw data from the API will not have any of your internal types applied to it yet, it'll be raw bytes or typed `string`. So I don't really see the connection between this and "I will still always be able to do `as OrgId` or `as UserId` so you need to do have some failure handling". Only your own trusted code can do "as OrgId", so... don't do that unless you have an OrgId. And once your own trusted code has made an OrgId, you don't need any runtime checking to see if it actually is an OrgId.

            > 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.

            What kind of things? Strings aren't mutable so you must be making a new string. But a new string will have a type like `string`, not `OrgId`, and then using it as an OrgId won't compile.

            • dvt 9 days ago

              > And once your own trusted code has made an OrgId, you don't need any runtime checking to see if it actually is an OrgId.

              Right, and once I have a verified OrgId, I'll just keep using the `myOrgId` variable throughout my code, and I don't really need branding. Maybe I can do type aliasing to make the code easier to read (type OrgId = string), but hardline type verification via branding seems moot unless you can make strong runtime guarantees. I mean, don't get me wrong, I think it's a cute novelty, but it doesn't really do anything.

              > But a new string will have a type like `string`, not `OrgId`, and then using it as an OrgId won't compile.

              Exactly. Maybe I'm wrong, but in a real codebase, I bet branding would probably just confuse people. "Why can't I change the last number of an OrgId?"—well, you see, once you do that, you lose the brand so now you need to manually do `as OrgId`.

              • Dylan16807 9 days ago

                > Right, and once I have a verified OrgId, I'll just keep using the `myOrgId` variable throughout my code, and I don't really need branding. Maybe I can do type aliasing to make the code easier to read (type OrgId = string), but hardline type verification via branding seems moot unless you can make strong runtime guarantees. I mean, don't get me wrong, I think it's a cute novelty, but it doesn't really do anything.

                I would rather put that information in the type system than in the variable name.

                It prevents passing the wrong variable, is that not useful?

                > Exactly.

                I don't see how what I said agrees with what you said. Making it not an OrgId prevents the weird blowups.

                A compilation error because you used the wrong type is not a blowup, it's preventing random blowups.

                And you shouldn't be shuffling digits using string code, that's the point. If you have a way to transmute OrgIds, it should be a function that returns an OrgId.

                I'd question whether people even need to know OrgId is a string.

                • dvt 9 days ago

                  > I'd question whether people even need to know OrgId is a string.

                  I mean, with numbers it's even more confusing (this might be a TS bug?):

                      type SpecialNumber = number & { __brand: "SpecialNumber" };
                      let n: SpecialNumber = 42 as SpecialNumber;
                      
                      n++;        // works (but should break)
                      n+=1;       // breaks
                      n = n + 1;  // breaks
                  
                  I understand the purpose behind it, I just think it's needlessly confusing and obtuse, and would be curious to see any serious code base that uses branding.
              • sophiabits 9 days ago

                This sounds like an argument against TypeScript in general, no?

                e.g. If I am parsing a string to a number via Number.parseInt, I don’t need a “: number” annotation because I can just call the variable “myNumber” and use that.

                Branding a string is in many ways an extension on the idea of “branding” my “myNumber” variable as “: number” rather than leaving it as “: any”. Even if the TS type system is easy to bail out of, I still want the type annotations in the first place because they are useful regardless. I like reducing the number of things I need to think about and shoving responsibility off to my tools.

              • kristiandupont 9 days ago

                If you have a function addMemberToOrg(memberId: string, orgId: string), you can accidentally call it like this: addMemberToOrg(myOrgId, myMemberId) and nobody will complain. With branded types, the compiler would mark it as an error.

                • dvt 9 days ago

                  Function signatures already solve that problem. We have all kinds of functions that take two numbers that mean different things (width/height, x/y, etc.). Branding seems like a solution looking for a problem. I just think it's too much overhead and confusion for too little gain.

                  In fact, a common pattern is to pass fully-qualified objects, e.g. `dimensions = {width: number, height: number}`, which makes mixing up variables even less likely since you have to explicitly specify them.

                  • kristiandupont 8 days ago

                    >Function signatures already solve that problem.

                    I literally just showed you how they don't. And you even go on to describe a pattern that makes the problem "even less likely" in the next sentence..

                    >Branding seems like a solution looking for a problem.

                    You do you.

          • dbalatero 10 days ago

            > 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!

      • DanielHB 9 days ago

        you can "x as unknown as Whatever" anywhere in the code as well. Or just use @ts-ignore

        You can do this kind of thing in "proper" statically typed languages as well like "(Whatever)((Object)x)

        The main problem in TS vs Java for this specific case is that if x is NOT Whatever then you get an error some point later in the code. In Java you would get the error immediately at the casting.

        It makes debugging a bit trickier but I don't really run into these kind of problems all that often in my TS code and when I do they are usually easy to track down.

  • wwwigham 10 days ago

    TypeScript _itself_ has a branded primitive string type it uses internally.[1] Dedicated syntax for creating unique subsets of a type that denote a particular refinement is a longstanding ask[2] - and very useful, we've experimented with implementations.[3]

    I don't think it has any relation to runtime type checking at all. It's refinement types, [4] or newtypes[5] depending on the details and how you shape it.

    [1] https://github.com/microsoft/TypeScript/blob/main/src/compil... [2] https://github.com/microsoft/TypeScript/issues/4895 [3] https://github.com/microsoft/TypeScript/pull/33038 [4] https://en.wikipedia.org/wiki/Refinement_type [5] https://wiki.haskell.org/Newtype

  • tshaddox 10 days ago

    > 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 10 days ago

      > 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 10 days ago

        > 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?

  • RHSeeger 10 days ago

    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 10 days ago

      > 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.

Slix 10 days ago

This is also useful for having a type that needs to be verified in some way before being used or trusted. UnverifiedLoginCookie vs. VerifiedLoginCookie

zbentley 10 days ago

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.

  • eropple 10 days ago

    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 10 days ago

    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.

  • shadowgovt 10 days ago

    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.

techn00 10 days ago

Very useful for DDD, like having an Email type, or String100 (string of 100 characters)

  • mason55 10 days ago

    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.

hankchinaski 10 days ago

Maybe is me but this seems way overengineered

  • efilife 9 days ago

    Everytime I read anything about typescript I think of this

herpdyderp 10 days ago

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>`

leecommamichael 10 days ago

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.

beders 10 days ago

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 10 days ago

    Nothing special about casts and type checks. It’s just a simpleish workaround for when nominal typing is more useful

  • klysm 10 days ago

    > Ah, the magical disappearing type system - now being used for nominal typing.

    More like: magical disappearing type system is not nominal: hacky workarounds ensue.

aleksiy123 10 days ago

Anyone else sometimes get more sucked in to perfectly typing stuff instead of writing code.

I guess working in the pure logic and formalism of types is just addictive.

I love it.

  • farnsworth 10 days ago

    Yes, if you do a good job it removes a lot of mental overhead for building the feature or refactoring it later.

    Although if you do a bad job it might add a lot...

gmdrd 8 days ago

Is there something wrong with creating opaque types using this method?

  type ObjectId = string & { 
  readonly __tag: unique 
  symbol }
This way, we don't need to use `never`, but we still prevent the creation of a structurally equivalent type by mistake.
SergeAx 9 days ago

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))`

rowanG077 10 days ago

How do you ensure someone doesn't reuse a brand string? Seems pretty brittle to me.

black_puppydog 9 days ago

Structural typing, and especially for primitives, with these work-arounds being so clunky and not built into the standard library, was the reason I started picking up Rust...

erik_seaberg 10 days ago

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 10 days ago

    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.

lang_agnostic 10 days ago

Isn't this just phantom types in other programming languages?

  • valcron1000 10 days ago

    More like `newtype` with implicit `coerce`

bazoom42 10 days ago

I think Typescript need nominal type aliases for primitives.

klysm 10 days ago

The downside of structural typing

jibbit 10 days ago

or a tagged type if you're old enough to have ever been exposed to another language

mirekrusin 10 days ago

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

  • recursive 10 days ago

    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.

    • mirekrusin 10 days ago

      It's the other way around.

      Handling difficult cases leads to more type errors.

      If type system is lax, it won't flag them but they can fail at runtime.

      React has very little of $FlowFixMe annotations for its codebase.

      In typescript projects on the other hand it's normal to see unsafety as normal code (casting, non null assertions, implicit and explicit anys etc).

      • recursive 9 days ago

        React vs "typescript projects" doesn't seem very fair. React would presumably be the pinnacle of style with respect to flow. Maybe not true, but it's not just some random project.

        Additionally, I've read a lot of typescript code, and the react codebase. My experience is different than yours. I see more type workarounds in react.

        FWIW "$FlowFixMe" occurs 822 times in commit hash cf5ab8b8b2c92523aba0b982fb403add053a9110 out of 498900 lines of *.js source. That includes blanks and comments. I don't have any good stats on the occurrence of type assertions in typescript.

        • mirekrusin 8 days ago

          Yes, it's probably not fair.

          I think you're double counting it (original files + emitted files)?

              git clone git@github.com:facebook/flow.git
              grep -roh '$FlowFixMe' flow | wc -l
              423
          
          Yes, it's not easy to grab good stats, my experience is that in ts projects you have much more of explicit type annotations vs flow which has better inference, a lot of type casting (unsafe), implicit and explicit anys (unsafe), null assertions (unsafe), unsafe OO etc.

          The code in flow has very interesting feel. Missing explicit type annotations are noticeable, feels like js. Optional names in type signature means functions have very Haskell'ish feel:

              const foo /*: number => string */ =
                x =>
                  x.toString()
          
          I use flow in comments so I don't have transpilation. Access to full language means a lot here.

          Typescript has some nice things that flow doesn't (ie. template literal types) - but recent activity in flow is very interesting. They've put a lot of effort into making flow understand typescript constructs. Many people don't realize how close they now became.

          Even some constructs are implemented first in flow (type guards, in ts scheduled for not yet released v5.5 I believe; NoInfer landed first in flow), then in typescript.

          It seems they have opportunity to make flow compatible with typescript enough that you could consume ts typedefs from flow - and when it happens it's definitely going to help opening doors wider for adoption.

          [0] https://github.com/search?q=repo%3Afacebook%2Fflow+%24FlowFi...

    • hu3 10 days ago

      Only hundreds? Not bad then.

      I expected thousands.

  • vundercind 10 days ago

    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 10 days ago

      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.

  • nsonha 10 days ago

    > 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 10 days ago

      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

      • nsonha 10 days ago

        I know what SOLID is, questioning how exactly you think Flow has "liskov substitution" as a feature.

        • mirekrusin 10 days ago

          L in SOLID refers to Liskov substitution principles.

          It's the only one from the SOLID list which can be typechecked - others are design principles.

          "Flow adhering to it" means that violating code will be flagged by type system.

          It matters because unlike other principles, violations can cause runtime errors.

          • nsonha 9 days ago

            > L in SOLID refers to Liskov substitution

            you think?

            > Flow adhering to it" means that violating code will be flagged by type system

            Shocking that type system can flag things

  • eyelidlessness 10 days ago

    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 10 days ago

      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 10 days ago

        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 10 days ago

          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 10 days ago

            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.

  • yen223 10 days ago

    Flow regularly crashed with bizarre segfaults errors, the last time I used it.

    • mirekrusin 8 days ago

      Sounds like vscode integration few years ago, yes.

  • baw-bag 10 days ago

    flow is dead (for jobs). I'm gonna annihilate Dart (apart from jobs)

    • mirekrusin 10 days ago

      java is great for jobs as well.

pprotas 10 days ago

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?

  • JJMalina 10 days ago

    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.

    • contextnavidad 10 days ago

      > 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.

    • LadyCailin 10 days ago

      That’s just plain encapsulation, if I understand you correctly. Branding, on the other hand, prevents complex types from being confused.

      • ebolyen 10 days ago

        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.

      • mason55 10 days ago

        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.

        • antifa 9 days ago

          Or double your GC burden by using a wrapper class.

  • chowells 10 days ago

    You've never had values that are only valid within a specific context and wanted to prevent using them incorrectly?

    • IshKebab 10 days ago

      Yeah structural typing assumes that all fields with the same name & type mean the same thing, but that clearly isn't always the case.

  • skybrian 10 days ago

    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.

  • brigadier132 10 days ago

    You don't use them but your favorite libraries do.

    • maeil 10 days ago

      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.

  • eyelidlessness 10 days ago

    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.

  • shadowgovt 10 days ago

    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.

  • CapsAdmin 10 days ago

    > The need for tricks like this (and other complicated types) is simply not there.

    It just depends on how constrained by types you want your code to be and how much time and effort you're willing to spend maintaining and writing code that fits within those constraints.

    Sometimes complicated types are introduced because you want to maintain editor features such as find by reference and the ability to refactor later. When it works, removing or adding new features feels fast, easy and safe.

    In the articles case you could differentiate between an email string type and a user id string type. Maybe sometime in the future you want to change the id to an integer instead, so now that it's already distinguished you could find all those places where that's applied.

    That's at least a selling point, in practice I've used this a few times but it doesn't come up that often. Sometimes the blast radius of a type isn't big so it's not worth doing.

  • odyssey7 10 days ago

    Nope, if you have lower expectations for your type system and are using TS then you are properly where you want to be.

  • amatecha 10 days ago

    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...