6 ms·
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
by spankalee 3y 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;
}
- deleted 3y ago[deleted]
- cush 3y agoSorry, 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 3y agoThis 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 3y agoAh, 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 3y agoThe function is great in cases where you can validate the string, or paired with lint rules that limit casting.
- galaxyLogic 3y agoThat'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 3y agoYes, 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).
- epolanski 3y agoWhich, 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 3y agoYeah in TS' own playground example they don't create a Symbol, they just intersect it inline: https://www.typescriptlang.org/play#example/nominal-typing https://www.typescriptlang.org/play#example/nominal-typing
- spankalee 3y agoNice. 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 3y 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 3y agoIsn'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 3y agoFrom 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/FAMwrgdgxgLglgewgAjgWwA4IM7bgIwBsBTACgA8AuZCYgN2ICcBKa2hx5Ab2GT+RgALRggDuNYuICijEY1IAiAMoI0xIXAgBzVJhx4ixZIICG2YyYwZitACYLmwAL7BgMAJ7XkAQWQBebgFPYmoAchNQ5CcAbjdg5AAhf0CPazD8SJjXVKNvBEYkgN8AH0TY4AB6CtQIGBtbYltkMGwTLWJQSFhEFAwRKGJcPIKKamGE5m5efmxROBgoQWQKADocyZ5+LeQoMyNw0OpGdTBGCFjt-l3sfYyjk7OLy+QGkBMwQhhqdCxcAhIKMwnnwXC5gMRyFhGDAgl4EowTBBbAAeAAqABpEgikcgIXUkeZsDBGJotAA+ZKo5AAMimW2OJlsSEI7mQAG0kposYimmZkAADAD6goAJFwEk5BfhsbZ+QBdNj0JixLJxLwAeXwACtiLAAJJNALwnnIokk7SY0KanX62yhMnlcDQeBIGjvLSCDyjZDW3UwA0baZ8H76f5kcgrYW+21SmWOMFO7quxm2UgmNhgND4JiY-AZrNMQP0h4oEzIADUyHwKtciZdKAg7s97gATN7o-7bEX+McYKdS7ZUxGo9q-QbYzzMcPBR3x9KeUDnMAgA https://www.typescriptlang.org/play?#code/FAMwrgdgxgLglgewgA...
- nsonha 3y agodo you need readonly with never?
- samtheprogram 3y agoYes, otherwise consumers could try reading the property (that doesn’t exist).
- nsonha 3y agohow would readonly fixes consumers reading the property?
- whilenot-dev 3y agoit doesn't. never prevents reading and readonly prevents (over)writing. GP just focused on the never-part of your question.
- nsonha 3y agonever means nothing can be assigned to it, so in that sense it's already readonly
- samtheprogram 3y agoCorrect, 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 3y agoIs all Typescript unreadable, or is that just your style?
- highwaylights 3y agoIt’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 3y agothis 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 3y agoIt'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.