Live data from Hacker News

Branded types for TypeScript

carlos-menezes.com

31–40 of 152 posts

Re: Branded types for TypeScript

#31
post #3

In most languages, doing what this article describes is quite straightforward: you would just define a new type (/ struct / class) called ‘Hash’, which functions can take or return. The language automatically treats this as a completely new type. This is called ‘nominal typing’: type equality is based on the name of the type. The complication with TypeScript is that it doesn’t have nominal typing. Instead, it has ‘st…

Typescript has good reasons to default to Structural Typing as untagged union type are one of the most used types in typing js code and Nominal Typing does not really have a good equivalent for them.

Re: Branded types for TypeScript

#32
post #18

Earlier quoted context omitted.

I see where you are coming from but you are not quite understanding what the OP was saying class A { public value: number } class B { public value: number } const x: A = new B() // no error This is structural typing (shape defines type), if typescript had nominal typing (name defines type) this would give an error. You could brand these classes to forcefully cause this to error. Branding makes structural typing work…

Indeed, this is what I was trying to say!

yeah this is such a common misconception, but give the class example I showed and people just get it.

"structural typing" and "nominal typing" are still quite new terms for most devs

Re: Branded types for TypeScript

#33
One nuance missing from the article is that since branded / tagged types extend from the base type, callers can still see and use string methods, which may not be what you want.

Equality can be problematic too. Imagine an Extension type, one could compare it with ".mp4" or "mp4", which one is correct?

Opaque types (that extend from `unknown` instead of T) work around these problems by forcing users through selector functions.

Re: Branded types for TypeScript

#34
post #29

Earlier quoted context omitted.

I see where you are coming from but you are not quite understanding what the OP was saying class A { public value: number } class B { public value: number } const x: A = new B() // no error This is structural typing (shape defines type), if typescript had nominal typing (name defines type) this would give an error. You could brand these classes to forcefully cause this to error. Branding makes structural typing work…

You can fix that fairly easily using private variables: class A { private value: number } class B { private value: number } const x: A = new B() // error You can also use the new Javascript private syntax (`#value`). And you can still have public values that are the same, so if you want to force a particular class to have nominal typing, you can add an unused private variable to the class, something like `private __f…

There is nothing to fix in my example, I was just highlighting the difference between nominal and structural typing. Adding a private field to the class is a form of branding (just like adding a Symbol key to a primitive).

Re: Branded types for TypeScript

#35
post #26

Earlier quoted context omitted.

I see where you are coming from but you are not quite understanding what the OP was saying class A { public value: number } class B { public value: number } const x: A = new B() // no error This is structural typing (shape defines type), if typescript had nominal typing (name defines type) this would give an error. You could brand these classes to forcefully cause this to error. Branding makes structural typing work…

The article describes making "number" a different type, not A and B. It's true that making A and B different is a unique problem of TypeScript, but making number a different type is a common issue in many languages.

number is a primitive, branding a primitive can be done like in the example. To brand a class you could also add a private field.

Some languages all values are objects and in those languages then the branding argument applies the same way. For languages with nominal typing and primitives you need to box the type yes. Regardless the core of the issue is understanding how structural typing works vs nominal typing

Re: Branded types for TypeScript

#37
As someone who values a tight domain model (a la DDD) and primarily writes TypeScript, I've considered introducing branded types many times, and always decline. Instead, we just opt for "aliases," especially of primatives (`type NonEmptyString = string`), and live with the consequences.

The main consequence is that we need an extra level of vigilance and discipline in PR reviews, or else implicit trust in one another. With a small team, this isn't difficult to maintain, even if it means that typing isn't 100% perfect in our codebase.

I've seen two implementations of branded types. One of them exploits a quirk with `never` and seems like a dirty hack that might no longer work in a future TS release. The other implementation is detailed in this article, and requires the addition of unique field value to objects. In my opinion, this pollutes your model in the same way that a TS tagged union does, and it's not worth the trade-off.

When TypeScript natively supports discriminated unions and (optional!) nominal typing, I will be overjoyed.

Re: Branded types for TypeScript

#38
post #21

It took me so long to fully appreciate TypeScript's design decision for doing structural typing vs. nominal typing. In all scenarios, including the "issue" highlighted in this article there is no reason for wanting nominal typing. In this case where the wrong order of parameters was the issue, you can solve it with [Template Literal Types]( https://www.typescriptlang.org/docs/handbook/2/template-lite... ). See [1]. A…

> A runtime bug is now a compile time bug.

This isn't valuable to you? How do you get this without nominal typing, especially of primatives?

Re: Branded types for TypeScript

#39
post #21

It took me so long to fully appreciate TypeScript's design decision for doing structural typing vs. nominal typing. In all scenarios, including the "issue" highlighted in this article there is no reason for wanting nominal typing. In this case where the wrong order of parameters was the issue, you can solve it with [Template Literal Types]( https://www.typescriptlang.org/docs/handbook/2/template-lite... ). See [1]. A…

How do you do this with template literal types? Does that mean you changed the string that gets passed at runtime? The nice thing about branding (or the "flavored" variant which is weaker but more convenient) is that it's just a type check and nothing changes at runtime.

The demo they posted demonstrates how to do it. But I don’t think it’s a generally good solution to the problem, it feels like it solves this specific case where the type is a string hash. I think the evolution of this for other types and objects is more like what the OP article suggests.

I wonder if a more natural solution would be to extend the String class and use that to wrap/guard things:

class Hash extends String {}

compareHash(hash: Hash, input: string)

Here's an example: https://www.typescriptlang.org/play/?#code/MYGwhgzhAEASkAtoF...

Post reply on HN