Live data from Hacker News

Branded types for TypeScript

carlos-menezes.com

41–50 of 152 posts

Re: Branded types for TypeScript

#41
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…

> And for `hash.toUpperCase()`, it's a valid program.

In a sense, but it's not the program we wanted to write, and types can be a useful way of moving that kind of information around within a program during development.

> TypeScript is not designed to stop you from using string prototype methods on... strings!

No, but it is designed to let me design my types to stop myself from accidentally using string prototype methods on data to which they don't actually apply, even when that data happens to be represented as... strings.

Re: Branded types for TypeScript

#42
post #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…

Can you say more about natively supporting discriminated unions?

You can already do this:

    type MyUnion = { type: "foo"; foo: string } | { type: "bar"; bar: string };
And this will compile:

    (u: MyUnion) => {
      switch (u.type) {
        case "foo":
          return u.foo;
        case "bar":
          return u.bar;
      }
    };
Whereas this wont:

    (u: MyUnion) => {
      switch (u.type) {
        case "foo":
          return u.bar;
        case "bar":
          return u.foo;
      }
    };

Re: Branded types for TypeScript

#43
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…

This example is also an odd choice because... it's not the right way to do it. If you're super concerned about people misusing hashes, using string as the type is a WTF in itself. Strings are unstructured data, the widest possible value type, essentially "any" for values that can be represented. Hashes aren't even strings anyway, they're numbers that can be represented as a string in base-whatever. Of course any such abstraction leaks when prodded. A hash isn't actually a special case of string. You shouldn't inherit from string.

If you really need the branded type, in that you're inheriting from a base type that does more things than your child type.... you straight up should not inherit from that type, you've made the wrong abstraction. Wrap an instance of that type and write a new interface that actually makes sense.

I also don't really get what this branded type adds beyond the typical way of doing it i.e. what it does under the hood, type Hash = string & { tag: "hash" }. There's now an additional generic involved (for funnier error messages I guess) and there are issues that make it less robust than how it sells itself. Mainly that a Branded inherits from a wider type than itself and can still be treated as a string, uppercased and zalgo texted at will, so there's no real type safety there beyond the type itself, which protects little against the kind of developer who would modify a string called "hash" in the first place.

Re: Branded types for TypeScript

#44
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…

You can still do this with classes in typescript:

class Hash extends String {}

https://www.typescriptlang.org/play/?#code/MYGwhgzhAEASkAtoF...

Re: Branded types for TypeScript

#45
post #29

Earlier quoted context omitted.

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

The point is that Typescript does have nominal typing. It's used if a class is declared with any kind of private member, and for `unique symbol`s. So both in the case I showed, and the case shown in the article, we are using true nominal types.

In fairness, we're also using branded types, which I think is confusing the matter here*. But they are specifically branded nominal types. We can also create structurally-typed brands (before the `unique symbol` technique, that was the only possible option). I think that's what the previous poster was referring to by "simulated nominal typing" — this is distinct from using `unique type` and private members, which are true nominal typing.

* Note: Branded types aren't necessarily a well-defined thing, but for the sake of the discussion let's define them so: a branded type is a type created by adding attributes to a type that exist at compile time but not at runtime.

Re: Branded types for TypeScript

#46
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…

[deleted]

Re: Branded types for TypeScript

#47

Earlier quoted context omitted.

You might need to read the article again, because those things are unrelated. Or you should explain what you mean.

I read the article, thanks. It makes the claims that (1) a string of a specific form (a hash) could be misused (eg. someone might call toUpper on it) or (2) passed in incorrect order to a function that takes multiple strings. Named parameters / outward-facing labels (Swift) completely solves (2). For (1), the solution is just ugly. Just use the type system in a normal manner and make a safe class “Hash” that doesn’t…

The point of branded types is, among other things, that you do not need to introduce a wrapper class which consumes additional memory.

Re: Branded types for TypeScript

#48

Alternatively for the case of id strings with known prefixes, a unique feature of TypeScript is you can use template string literal types: https://www.kravchyk.com/adding-type-safety-to-object-ids-ty...

Note that the prefix was never intended to be looked at as the real problem. That's not a hash function, that's an example hash function because TFA couldn't be bothered to implement a proper one. They're not actually trying to solve the prefix problem.

Re: Branded types for TypeScript

#50
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…

Template literal types solve ordering for a very specific type of parameter-order problems which happens to include the (explicitly identified as an example) terrible hash function that just prepends "hashed_".

But what about when you have an actual hash function that can't be reasonably represented by a template literal type? What about when the strings are two IDs that are different semantically but identical in structure? What about wanting to distinguish feet from inches from meters?

Don't get me wrong, I like structural typing, but there are all kinds of reasons to prefer nominal in certain cases. One reason why I like TypeScript is that you can use tricks like the one in TFA to switch back and forth between them as needed!

Post reply on HN