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.
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.
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.
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.
… 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.
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;
}
}
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__.
> 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;
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”.
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.
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.
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.