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;
}
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
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).
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".
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?)
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.
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.
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.
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.
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:
Use it like: And the hover-over type is: