Hacker News new | past | comments | ask | show | jobs | submit login

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;
    }



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; }'


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;
    }


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.


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


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


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


Does it though?


Yes.


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.


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?


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


I personally prefer less code and more explicit casting


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


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


Agree. I was talking about

    const accountId = AccountId (“43525”)
being less explicit


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.


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


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.


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


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


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


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.


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


do you need readonly with never?


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


how would readonly fixes consumers reading the property?


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

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


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


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.


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


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


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


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?


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.


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.


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.


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.


> 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


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.


Equivalent code in Python:

  from typing import NewType


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




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: