NHacker Next
  • new
  • past
  • show
  • ask
  • show
  • jobs
  • submit
TypeScript: Branded Types (prosopo.io)
spankalee 10 days 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;
    }
cush 10 days ago [-]
Sorry, 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 10 days ago [-]
This 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 10 days ago [-]
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

    const accountId = "125314" as AccountId
It makes sense that the technique uses casting.
spankalee 10 days ago [-]
The function is great in cases where you can validate the string, or paired with lint rules that limit casting.
10 days ago [-]
cush 10 days ago [-]
At that point I'd just use a class constructor.
spankalee 10 days ago [-]
That has a lot of overhead compared to validating and casting a primitive.
cush 9 days ago [-]
Does it though?
gliptic 9 days ago [-]
Yes.
yencabulator 9 days ago [-]
And herein lies one of the worst features of Typescript.

    const myCompanyId = something_complicated as CompanyId;
where something_complicated actually is a UserId, not a bare string.

It is way too easy to accidentally destroy type safety with `as`, there are absolutely 0 safeguards.

I fear every single instance of `as` in any Typescript source I see.

galaxyLogic 10 days ago [-]
That'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 10 days ago [-]
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).
cush 10 days ago [-]
I personally prefer less code and more explicit casting
whilenot-dev 9 days ago [-]

  const accountId = "125314" as AccountId;
or

  const accountId = new AccountId("125314");
or

  const accountId = accountIdFromString("125314");
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".

[0]: https://lexi-lambda.github.io/blog/2019/11/05/parse-don-t-va...

usrusr 9 days ago [-]
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?)

cush 9 days ago [-]
Agree. I was talking about

    const accountId = AccountId (“43525”)
being less explicit
epolanski 10 days ago [-]
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.

amatecha 10 days ago [-]
Yeah in TS' own playground example they don't create a Symbol, they just intersect it inline: https://www.typescriptlang.org/play#example/nominal-typing
spankalee 10 days ago [-]
Nice.

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 10 days 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 10 days ago [-]
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).

wk_end 10 days ago [-]
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.

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

eyelidlessness 10 days ago [-]
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.

lmm 10 days ago [-]
> 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.

10 days ago [-]
nsonha 10 days ago [-]
do you need readonly with never?
samtheprogram 10 days ago [-]
Yes, otherwise consumers could try reading the property (that doesn’t exist).
nsonha 10 days ago [-]
how would readonly fixes consumers reading the property?
whilenot-dev 9 days ago [-]
it doesn't. never prevents reading and readonly prevents (over)writing.

GP just focused on the never-part of your question.

nsonha 9 days ago [-]
never means nothing can be assigned to it, so in that sense it's already readonly
samtheprogram 7 days ago [-]
Correct, 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 10 days ago [-]
Is all Typescript unreadable, or is that just your style?
spankalee 9 days ago [-]
Are all of your comments devoid of content, or just this one?
38 9 days ago [-]
Are all of your comments devoid of content, or just this one?
highwaylights 10 days ago [-]
It’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 9 days ago [-]
this 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 9 days ago [-]
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.

38 9 days ago [-]
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.
scotty79 8 days ago [-]
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.

38 8 days ago [-]
> languages like go that intentionally lack features

people will go as far down the rabbit hole as you let them, which is what the Go developers understand and are trying to account for:

https://www.hyrumslaw.com

scotty79 8 days ago [-]
I understand it's merit. I just don' like it. I think it's at least 100 years too soon for that. Exploration is important.
gmdrd 7 days ago [-]
Equivalent code in Python:

  from typing import NewType
mock-possum 9 days ago [-]
I mean you’re not wrong, this is getting pretty far into the weeds in terms of typescript syntax.
thepaulmcbride 10 days ago [-]
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.

CharlieDigital 10 days ago [-]
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.

[0] https://developer.mozilla.org/en-US/docs/Web/JavaScript/Refe...

thepaulmcbride 10 days ago [-]
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.
tadfisher 10 days ago [-]
That's a weird objection, because Typescript classes are literally Javascript classes[1].

[1]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Refe...

afavour 10 days ago [-]
There was plenty of outrage when classes were added to JS. “JS uses prototypal inheritance, not OOP!”
krapp 10 days ago [-]
I'm still a bit cranky about that, but because objects should be good enough for anyone. They aren't even really classes anyway.
Aeolun 10 days ago [-]
If it walks like a duck, and quacks like a duck, for all intents and purposes, it is a duck.
chrysoprace 10 days ago [-]
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.
eyelidlessness 9 days ago [-]
> The absolutism towards OOP/FP

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

schwartzworld 9 days ago [-]
JS classes are a great fit for FP. They're just functions with closures and a little syntactic sugar.
svieira 10 days ago [-]
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;
      }
    }
tshaddox 10 days ago [-]
> As far as I understand classes are the only real way to get nominal typing in TypeScript.

Although classes and instances are ultimately structural, right?

    let foo = new Foo();
    assert(foo.constructor == Foo);
chrismorgan 9 days ago [-]
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__.
thepaulmcbride 10 days ago [-]
Yep, you’re absolutely right
theteapot 10 days ago [-]
> 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;
superamadeus 10 days ago [-]
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”.
thepaulmcbride 10 days ago [-]
You’re right. This is what I meant, but I was not at all clear in my original comment.
DanielHB 9 days ago [-]
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.

spankalee 10 days ago [-]
Yeah, classes are generally better than branded types for objects (unless you're just building discriminated unions).

What's particularly better these days is that you get nominal typing and brand checks with standard private fields:

    class Foo {
      #brand;

      static isFoo(o): o is Foo {
        return #brand in o;
      }
    }
iainmerrick 9 days ago [-]
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.

mbwgh 9 days ago [-]
Is this different/better than using `instanceof`?
superamadeus 10 days ago [-]
This is really neat, thank you for sharing!
epolanski 10 days ago [-]
An example of what you can implement on top of branded types that I want to share with fellow hackers:

- currencies

You may have some sort of integer representing the number of cents of some currency and you want to avoid doing operations between the wrong currencies (such as adding euros and pesos).

You can create branded types and functions that work on those Euro branded numbers and then decide how to do math on it. Or numbers representing metals and such.

It's useful in other scenarios such as a, idk, strings, you could theoretically brand strings as idk ASCII or UTF-8 or the content of an http body to avoid mixing those up when encoding but I want to emphasize that often many of those hacks are easier to be handled with stuff like dictionaries or tagged unions.

An example of what can be achieved with similar approaches (beware it's oriented for people that are at least amateur practitioners of functional programming) is Giulio Canti's (an Italian mathematician and previously author of t-comb, fp-ts, io-ts, optic-ts, and now effect and effect/schema), the money-ts library:

https://github.com/gcanti/money-ts

spankalee 10 days ago [-]
As much as I love branding, I would be hesitant to use it for critical math and where you might want to inspect the type at runtime as with money.

Branding is easily circumvented, so it's best as a developer hint that helps document APIs and alerts us to common mistakes as an incremental improvement over primitives.

For money and similar I would use objects and a custom math library.

epolanski 9 days ago [-]
You can parse before brand thus enhance and enforce runtime correctness.
vaylian 9 days ago [-]
Another example: inches versus meters. We have lost a mars probe due to a misinterpretation of the numbers: https://www.latimes.com/archives/la-xpm-1999-oct-01-mn-17288...
atonse 10 days ago [-]
Thanks, the currency stuff was a great real-world example that crystallized where this would be nice to have at a core level with typescript to severely reduce the odds of doing cross-currency math.
robocat 9 days ago [-]
I have seen plenty of currencies come and go in my life: weird to lock that into your codebase.

And I'm struggling to think of where in a codebase I might want to hardcode Pesos or Euros.

jonathanlydall 10 days ago [-]
I can see this being useful and it seems about as neat a solution as you can currently get in TypeScript as it stands today, but it’s still cumbersome.

My feeling is that while you can do this, you’re swimming upstream as the language is working against you. Reaching for such a solution should probably be avoided as much as possible.

kristiandupont 10 days ago [-]
I don't see why. I greatly prefer typescript's structural typing for almost everything. But id's in data models are an exception, so I use branding for those. It works perfectly, the only overhead is in the write-once declaration and now I am protected from accidentally using an AccountId where a MemberId was expected, even though they are both just strings.
amatecha 10 days ago [-]
How do ids of different types accidentally get into a place they shouldn't be? Is this simply a case where someone mistakenly passes along a property that happens to be called "id", not noticing it's an account id rather than a member id (as in, an implementation error)?
klysm 10 days ago [-]
This kind of mistake is quite easy to make, especially when somebody writes a function that takes several IDs as arguments next to each other. I've seen it happen a number of times over the years
z3t4 9 days ago [-]

    function foo(customerId, itemId, orderId) {
        if(!customer[customerId]) throw new Error("Customer with id=" + customerId + " does not exist");
        if(!item[itemId]) throw new Error("Item with id=" + itemId + " does not exist");
        if(!order[orderId]) throw new Error("Order with id=" + orderId+ " does not exist");
    }
Just an example of how you can detect errors early with defensive programming.
antifa 9 days ago [-]
As oppose to a zero cost build time check, that's 3 expensive runtime DB checks, plus an account and a member could have the same ID, so it might run anyways but on the wrong DB rows.
z3t4 7 days ago [-]
You made two points that are correct in theory, but in practice those lookups would/could take micro-seconds, and the bug would be detected within minutes in a production environment. And it would take five minutes or less to patch.
antifa 6 days ago [-]
If you're cowboy coding as a solo dev, sure.
z3t4 2 days ago [-]
We could argue if having 1-3 code cowboys is better then a large team. If the code cowboys can produce higher quality, quicker. They will be difficult to replace after many years though.
klysm 9 days ago [-]
Yeah this is exactly why using primitive types for ids is bad. If you encode it in the type system then you cannot make this error and don't need to check it at runtime.
z3t4 7 days ago [-]
Most bugs are found at runtime. The static analysis mostly finds silly errors that are easy to fix. A great type system do make it easier to write spaghetti code though, and you can have one letter variable names - naming is difficult.
sunshowers 10 days ago [-]
A lot of code may end up dealing with multiple kinds of IDs at the same time.

For Rust I wrote newtype-uuid to provide newtype wrappers over UUIDs, which has already found a couple of bugs at Oxide.

[1] https://crates.io/crates/newtype-uuid

mason55 10 days ago [-]
Yeah, or what's more likely is that you originally used an AccountID everywhere, but then you decided that you want to use a UserAccountID. So you update your functions but you miss a spot when you're updating all your call sites.

Or you have a client library that you use to interact with your API, and the client library changes, and you don't even notice.

Or you change the return type of a function in a library, and you don't even know who all the callers are, but it sure would be nice if they all get a build error when they update to the latest version of your library.

Lots and lots and lots of ways for this to happen in a medium+ sized project that's been around for more than a few months. It's just another way to leverage the power of types to have the compiler help you write correct code. Most of the time most people don't mess it up, but it sure feels good to know that it's literally impossible to mess up.

mejutoco 10 days ago [-]
If you have a distance argument typed as a number, for example, and you pass miles instead of km.

It is equivalent to newtype in haskell.

Another example somebody mentioned here is a logged in user. A function can take a user and we need to always check that the user is logged. Or we could simply create a LoggedUser type and the compiler will complain if we forget.

sophiabits 9 days ago [-]
Happens a lot with junction tables ime. e.g. At my last job we had three tables: user, stream, user_stream. user_stream is an N:N junction between a user and a stream

A user is free to leave and rejoin a stream, and we want to retain old data. So each user_stream has columns id, user_id, stream_id (+ others)

Issues occur when people write code like the following:

streamsService.search({ withIds: userStreams.map((stream) => stream.id), });

The issue is easily noticed if you name the “stream” parameter “userStream” instead, but this particular footgun came up _all_ the time in code review; and it also occurred with other junction tables as well. Branded types on the various id fields completely solve this mistake at design time.

cush 10 days ago [-]
Yes! I've wanted this in Typescript for ages! Is it possible to brand primitives though?
kristiandupont 10 days ago [-]
Yes you can, it works just the same :-)
cush 10 days ago [-]
Sorry, with the Symbol? How does it work?
maskros 10 days ago [-]
You don't even need the symbol. If you want the simplest thing that will work:

    type Velocity = number & { BRAND: "Velocity" }
    type Distance = number & { BRAND: "Distance" }

    var x = 100 as Distance
shadowgovt 10 days ago [-]
> as the language is working against you

Right, but this is one of those situations where a reasonable person could conclude "The language is not working to help me solve the problem I'm trying to solve." Sometimes you don't want JavaScript sloppiness and you also don't want TypeScript structural-type "sloppiness," desiring instead nominal types.

This is a clever way to use the existing infrastructure to get nominal types with no additional runtime overhead.

jrockway 10 days ago [-]
Rather, I think Typescript's philosophy of not having a runtime is simply wrong. Other statically typed languages retain type information at runtime. I don't understand why that was so important to explicitly exclude from Typescript.
nosefurhairdo 10 days ago [-]
If we needed a special Typescript runtime to use Typescript it would become nearly useless. The vast majority of Typescript becomes JavaScript running in Node or V8.

The web is stuck with JavaScript, and Typescript is a tool to help us write maintainable JavaScript.

williamcotton 10 days ago [-]
Could’t one write a TypeScript runtime in JavaScript or WASM?
DanielHB 9 days ago [-]
Using any JIT language (most implementations of python, Java, ruby, JS, etc) in WASM means porting the runtime to WASM and then having the runtime (in WASM) parse and JIT-compile your code incurring a double-whammy of performance degradation (WASM runtime compiling your JIT language runtime and then your JIT language runtime compiling your actual code).

To do this kind of thing viable there are two ways I can think of:

1) proper ahead-of-time compiler for your language targeting WASM directly. But that means pretty much rebuilding the whole stack for the language as that is a completely different approach.

2) Do something like Android Runtime (ART) does which translates Java bytecode to native machine instructions whenever you install an android app. But in this case translate the bytecode to WASM and do it before distribution. This requires a new compiler-backend which is still quite complex.

Both of these mean you don't have a language runtime at all. There is a reason most WASM stuff you see is in written C/C++/Rust and it is not just the lack of GC.

kevingadd 10 days ago [-]
It wouldn't be very useful, since a TS-in-JS runtime would have significant performance overhead, and a TS-in-WASM runtime would have very expensive JS interop plus cross-language-GC hurdles. Might be less bad with WASM GC.
DonHopkins 10 days ago [-]
Does branding work in AssemblyScript?
moritzwarhier 10 days ago [-]
With horrible overhead, and in the case of WASM, lots of serialization and API glueing (DOM is just somewhere near the tip of the iceberg) woes, maybe?

Would be a fun thing to do for sure, but never as fast as the APIs built into the browser runtime.

vundercind 10 days ago [-]
So it’s Just JavaScript and the risk of adopting it is basically zero.

It’s the thing that made it an easy sell after everyone got turned off by the long-term experience of Coffeescript and such. It’s the reason various “better” typed languages that run on top of JS have flopped except with enthusiasts.

recursive 10 days ago [-]
Static typing information isn't retained at runtime. e.g.

    ListInterface list = new ConcreteList();
    let runtimeType = GetTypeOf(list); // will be `ConcreteList`, not `ListInterface`
This is an imaginary java-like language, but I'm not aware of a statically typed language that gives you the static type resolutions at run-time, outside of cases like implicit generic resolutions and things like that.
Quekid5 10 days ago [-]
> Other statically typed languages retain type information at runtime.

Funnily enough, Haskell[0] doesn't... unless you ask it to by explicitly asking for it via Typable[1].

[0] ... which is renowned/infamous for its extremely static+strong typing discipline. It is nice that one can opt in via Typeable, but it's very rare to actually need it.

[1] https://hackage.haskell.org/package/base-4.19.1.0/docs/Data-...

klysm 10 days ago [-]
I disagree. If we had a runtime we'd have to inject that all over the place and compatibility with javascript wouldn't be a given.
mason55 10 days ago [-]
The decision between structural and nominal typing has nothing to do with whether you retain type information at runtime.

TS could have just as easily chosen nominal typing + a simple way to do typedefs and had everything else work like it does now. But structural typing gives you a lot of other useful features.

hpeter 10 days ago [-]
Typescript is not a real standalone language. It's just javascript after all.

Try Dart, maybe you like it. It's like TS but with it's own runtime.

munk-a 10 days ago [-]
Isn't this just the classic issue of inferred typing coming back to bite us in the way everyone originally predicted? Go runs into the same issue where wildly different types may be considered the same based purely on coincidental naming and matching against interfaces the original authors had no intent to match against. At the end of the day I think the easier system to work with is one in which all type compatibility needs to be explicitly declared - if your array is iterable defining that it implements iterable lets your compiler shout at you if iterable suddenly gets another abstract method that you didn't implement - and it makes sure that if you add a method `current` to a class it doesn't suddenly means that it properly supports iterable-ity.

Determining types by what things appear to do instead of what they state they do seems like a generally unnecessary compromise in typing safety that isn't really worth the minuscule amount of effort it can save.

cageface 10 days ago [-]
The other problem I've found with structural types in Typescript is that it can lead to some very complex type specifications. Instead of carefully thinking about and naming the entities in your system you can get some very hard to parse type spaghetti.
skybrian 10 days ago [-]
I haven’t run into interfaces coincidentally matching in Go. Have you? It might happen more easily for primitive types, though, and Go does have a way to declare new types:

   type UserId string
klysm 9 days ago [-]
Are these simple aliases or nominally distinct types?
yencabulator 9 days ago [-]
Aeolun 10 days ago [-]
I mean, they have to stay backwards compatible with Javascript. That kind of limits your options.
munk-a 9 days ago [-]
Not particularly - they could use distinct tokens (i.e. these brandings) on all types implicitly.
JJMalina 10 days ago [-]
Zod has branded types: https://zod.dev/?id=brand it works really nicely with Zod schemas
dvt 10 days ago [-]
Weird idea, as types in TS are structural by design. If this is something you need, it smells like "runtime checking" not amending the type system.
williamdclt 10 days ago [-]
Design doesn’t cover all use cases. There’s nothing weird about wanting different types for “user id” and “organisation id” so that you don’t use the wrong argument by mistake
dvt 10 days ago [-]
While you're right that branding makes passing arguments more ergonomic, I will still always be able to do `as OrgId` or `as UserId` so you need to do have some failure handling anyway, unless you're okay with blowing up in the user's face.
williamdclt 10 days ago [-]
That’s nothing specific to branded types. We know Typescript doesn’t enforce anything at runtime, whether it’s primitive types, complex types, nullability or anything else, that’s nothing new.

It’s not about security, it’s about safety: make it harder to do the wrong thing, and make it easier to do the right thing than the wrong one.

strongly-typed 10 days ago [-]
The way you're describing it sounds to me like you are adding failure handling to handle cases where the programmer is intentionally misusing the thing. I would argue that in this type of situation, the error handling is not necessary because it would hide the fact that the thing is being misused.

Or perhaps I'm misunderstanding your comment. When you do `as OrgId` or `as UserId`, where do you envision those casts in ways that would require handling failures?

dvt 10 days ago [-]
My point is that the API consumer does not guarantee types (say it's REST or something), so the assumption that the string you send it will always be the right type (or call it "format") seems like a bad one. Unless you control API usage from end-to-end (which kind of defeats the point of an API), you need error checking (or at least exceptions to bubble up).

A lot of times branding is used to mark data received from a database, e.g.: this field is an `OrgId`, but I can do all kinds of things to that string which might make it not-an-`OrgId` at any point. Then, I'll try to reuse it the `OrgId`, and I'll get some weird error or blowup and I'll have no idea why. So the point is that (a) branding is a dubious feature because it can obfuscate soft-type-breakage (I call it "soft" because at the end of the day, we're just dealing with strings), and (b) it still doesn't preclude runtime error checking unless you're okay with blowups.

Dylan16807 9 days ago [-]
> My point is that the API consumer does not guarantee types (say it's REST or something), so the assumption that the string you send it will always be the right type (or call it "format") seems like a bad one.

The raw data from the API will not have any of your internal types applied to it yet, it'll be raw bytes or typed `string`. So I don't really see the connection between this and "I will still always be able to do `as OrgId` or `as UserId` so you need to do have some failure handling". Only your own trusted code can do "as OrgId", so... don't do that unless you have an OrgId. And once your own trusted code has made an OrgId, you don't need any runtime checking to see if it actually is an OrgId.

> A lot of times branding is used to mark data received from a database, e.g.: this field is an `OrgId`, but I can do all kinds of things to that string which might make it not-an-`OrgId` at any point.

What kind of things? Strings aren't mutable so you must be making a new string. But a new string will have a type like `string`, not `OrgId`, and then using it as an OrgId won't compile.

dvt 9 days ago [-]
> And once your own trusted code has made an OrgId, you don't need any runtime checking to see if it actually is an OrgId.

Right, and once I have a verified OrgId, I'll just keep using the `myOrgId` variable throughout my code, and I don't really need branding. Maybe I can do type aliasing to make the code easier to read (type OrgId = string), but hardline type verification via branding seems moot unless you can make strong runtime guarantees. I mean, don't get me wrong, I think it's a cute novelty, but it doesn't really do anything.

> But a new string will have a type like `string`, not `OrgId`, and then using it as an OrgId won't compile.

Exactly. Maybe I'm wrong, but in a real codebase, I bet branding would probably just confuse people. "Why can't I change the last number of an OrgId?"—well, you see, once you do that, you lose the brand so now you need to manually do `as OrgId`.

Dylan16807 9 days ago [-]
> Right, and once I have a verified OrgId, I'll just keep using the `myOrgId` variable throughout my code, and I don't really need branding. Maybe I can do type aliasing to make the code easier to read (type OrgId = string), but hardline type verification via branding seems moot unless you can make strong runtime guarantees. I mean, don't get me wrong, I think it's a cute novelty, but it doesn't really do anything.

I would rather put that information in the type system than in the variable name.

It prevents passing the wrong variable, is that not useful?

> Exactly.

I don't see how what I said agrees with what you said. Making it not an OrgId prevents the weird blowups.

A compilation error because you used the wrong type is not a blowup, it's preventing random blowups.

And you shouldn't be shuffling digits using string code, that's the point. If you have a way to transmute OrgIds, it should be a function that returns an OrgId.

I'd question whether people even need to know OrgId is a string.

dvt 9 days ago [-]
> I'd question whether people even need to know OrgId is a string.

I mean, with numbers it's even more confusing (this might be a TS bug?):

    type SpecialNumber = number & { __brand: "SpecialNumber" };
    let n: SpecialNumber = 42 as SpecialNumber;
    
    n++;        // works (but should break)
    n+=1;       // breaks
    n = n + 1;  // breaks
I understand the purpose behind it, I just think it's needlessly confusing and obtuse, and would be curious to see any serious code base that uses branding.
sophiabits 9 days ago [-]
This sounds like an argument against TypeScript in general, no?

e.g. If I am parsing a string to a number via Number.parseInt, I don’t need a “: number” annotation because I can just call the variable “myNumber” and use that.

Branding a string is in many ways an extension on the idea of “branding” my “myNumber” variable as “: number” rather than leaving it as “: any”. Even if the TS type system is easy to bail out of, I still want the type annotations in the first place because they are useful regardless. I like reducing the number of things I need to think about and shoving responsibility off to my tools.

kristiandupont 9 days ago [-]
If you have a function addMemberToOrg(memberId: string, orgId: string), you can accidentally call it like this: addMemberToOrg(myOrgId, myMemberId) and nobody will complain. With branded types, the compiler would mark it as an error.
dvt 9 days ago [-]
Function signatures already solve that problem. We have all kinds of functions that take two numbers that mean different things (width/height, x/y, etc.). Branding seems like a solution looking for a problem. I just think it's too much overhead and confusion for too little gain.

In fact, a common pattern is to pass fully-qualified objects, e.g. `dimensions = {width: number, height: number}`, which makes mixing up variables even less likely since you have to explicitly specify them.

kristiandupont 8 days ago [-]
>Function signatures already solve that problem.

I literally just showed you how they don't. And you even go on to describe a pattern that makes the problem "even less likely" in the next sentence..

>Branding seems like a solution looking for a problem.

You do you.

dbalatero 10 days ago [-]
> Unless you control API usage from end-to-end (which kind of defeats the point of an API)

Isn't this all frontend client bundles that talk to their own private backend API? Those are controlled end-to-end. My company has one, yours probably does too!

DanielHB 9 days ago [-]
you can "x as unknown as Whatever" anywhere in the code as well. Or just use @ts-ignore

You can do this kind of thing in "proper" statically typed languages as well like "(Whatever)((Object)x)

The main problem in TS vs Java for this specific case is that if x is NOT Whatever then you get an error some point later in the code. In Java you would get the error immediately at the casting.

It makes debugging a bit trickier but I don't really run into these kind of problems all that often in my TS code and when I do they are usually easy to track down.

wwwigham 10 days ago [-]
TypeScript _itself_ has a branded primitive string type it uses internally.[1] Dedicated syntax for creating unique subsets of a type that denote a particular refinement is a longstanding ask[2] - and very useful, we've experimented with implementations.[3]

I don't think it has any relation to runtime type checking at all. It's refinement types, [4] or newtypes[5] depending on the details and how you shape it.

[1] https://github.com/microsoft/TypeScript/blob/main/src/compil... [2] https://github.com/microsoft/TypeScript/issues/4895 [3] https://github.com/microsoft/TypeScript/pull/33038 [4] https://en.wikipedia.org/wiki/Refinement_type [5] https://wiki.haskell.org/Newtype

tshaddox 10 days ago [-]
> If this is something you need, it smells like "runtime checking" not amending the type system.

This is both! The primary point of branded types is to allow you to use the type system to ensure that a particular runtime check has taken place.

dvt 10 days ago [-]
> The primary point of branded types is to allow you to use the type system to ensure that a particular runtime check has taken place.

I see. This is kind of cool, though the branding can still be broken via down-the-stream mutations. Would be nice to enforce re-branding every time a variable is changed, but that seems like a lot of overhead.

tshaddox 10 days ago [-]
> the branding can still be broken via down-the-stream mutations

Only for mutable values! A branded string should be as immutable as they come, right?

RHSeeger 10 days ago [-]
Right, but there are times when structural typing is not the right choice. And runtime checking is ... suboptimal. I mean, saying runtime checking is the right choice is like saying a type system isn't necessary, because you have automated tests. They're great, and they're a tool that provides value... but so too are types.
dbalatero 10 days ago [-]
> And runtime checking is ... suboptimal.

Especially on the frontend, where your `throw new Error("Bad input type")` might brick the entire app if uncaught. I'd much rather hear an earful from TypeScript before a bundle is ever produced.

Slix 10 days ago [-]
This is also useful for having a type that needs to be verified in some way before being used or trusted. UnverifiedLoginCookie vs. VerifiedLoginCookie
zbentley 10 days ago [-]
It's a clever trick, but the compiler errors leave a lot to be desired. If a TS library makes heavy use of nominal (branded/distinct) types in a domain where accidentally passing values of the wrong type is common, I can imagine a lot of library users being more confused, not less, by code that uses this approach.

The article reads more like an endorsement of languages that do structurally-aware nominal typing (that is, languages that are nominally typed but that have awareness of structural equivalence so that zero-cost conversions and intelligible compile-time errors for invalid conversions are first class) than a persuasive case for the symbol-smuggling trick described.

eropple 10 days ago [-]
Generally speaking, I wouldn't use branded inputs for libraries. Branded types make a lot more sense to me when working in business-logic cases, to identify data at the edge of a bounded context and tracing through the system. A library is downstream of that and the code that requires the branded type should be controlling the inputs to the library.
eyelidlessness 10 days ago [-]
I’ve found TypeScript errors can be made a lot easier to understand with some forethought about how invalid types are defined in the first place. For instance, a many-branched conditional type may be much easier to understand (both as a library user and as a reviewer/maintainer of the library code itself) if each `never` (or whatever actual failure case) is instead a string literal describing the nature of the failure. And better still if it can be templated with an aspect of the input type to highlight what aspect of it triggered the error.
shadowgovt 10 days ago [-]
This is one of those frustrations I encounter with C++.

C++'s templating lets express some very powerful type constraints... But good luck reading the compiler errors generated by someone else's very powerful type constraints that you haven't fully grokked to an implementation level.

techn00 10 days ago [-]
Very useful for DDD, like having an Email type, or String100 (string of 100 characters)
mason55 10 days ago [-]
Works very well too for any kind of validation or encoding. Anything that accepts input from the outside world can accept a string. And then everything else in the app can work with a "SafeString" and the only way to create a safe string is to send a string through a string escape function (or whatever makes sense for your app).

Works especially well if you're using any kind of hexagonal architecture, make your functional core only accept validated/escaped/parsed/whatever types, and then the imperative shell must send any incoming data through whatever transformation/validation/etc before it can interact with the core.

hankchinaski 10 days ago [-]
Maybe is me but this seems way overengineered
efilife 9 days ago [-]
Everytime I read anything about typescript I think of this
herpdyderp 10 days ago [-]
The implementation for `RemoveBrand` is incorrect: it currently grabs all property types of `T`, it's not removing the property `[brand]` from `T`. It should be `Omit<T, typeof brand>`
dimitrisnl 10 days ago [-]
Selfish plug about the same topic https://dnlytras.com/blog/nominal-types
kevincox 9 days ago [-]
I also wrote something like this and came up with something similar to yours: https://kevincox.ca/2023/03/21/lying-to-typescript/#summary

However I just inlined the "tag". Also like yours but unlike OP there is no runtime tag. Which can be a pro (no extra code) or a con (can't do runtime checks).

leecommamichael 10 days ago [-]
The Odin language has a ‘distinct’ type-qualifier which accomplishes this tersely. It’s a big part of the type system, and C’s type-system for that matter.
beders 10 days ago [-]
Ah, the magical disappearing type system - now being used for nominal typing.

I'm curious to see what the JS code looks like for casts and type checks in that case.

williamdclt 10 days ago [-]
Nothing special about casts and type checks. It’s just a simpleish workaround for when nominal typing is more useful
klysm 10 days ago [-]
> Ah, the magical disappearing type system - now being used for nominal typing.

More like: magical disappearing type system is not nominal: hacky workarounds ensue.

aleksiy123 10 days ago [-]
Anyone else sometimes get more sucked in to perfectly typing stuff instead of writing code.

I guess working in the pure logic and formalism of types is just addictive.

I love it.

farnsworth 10 days ago [-]
Yes, if you do a good job it removes a lot of mental overhead for building the feature or refactoring it later.

Although if you do a bad job it might add a lot...

gmdrd 8 days ago [-]
Is there something wrong with creating opaque types using this method?

  type ObjectId = string & { 
  readonly __tag: unique 
  symbol }
This way, we don't need to use `never`, but we still prevent the creation of a structurally equivalent type by mistake.
satvikpendem 10 days ago [-]
Reminds me of another sort of type-driven development, making invalid states unrepresentable: https://geeklaunch.io/blog/make-invalid-states-unrepresentab...
SergeAx 9 days ago [-]
Seems like patching earlier language architecture mistake. If you create custom types, you should not use duck typing on them. You should use typecasting, so the programmer would write something like `result = processEntityA(A(B))`
rowanG077 10 days ago [-]
How do you ensure someone doesn't reuse a brand string? Seems pretty brittle to me.
9 days ago [-]
black_puppydog 9 days ago [-]
Structural typing, and especially for primitives, with these work-arounds being so clunky and not built into the standard library, was the reason I started picking up Rust...
9 days ago [-]
erik_seaberg 10 days ago [-]
This works because casts are allowed to quietly create values whose types are wrong. It would have been better if the cast added a runtime check, or at least we distinguish sound (checked) and unsound casts the way C++ does.

I think Haskell avoid this by actually requiring you to write sound conversion functions between phantom types (it helps that phantom types don't involve any visible state at runtime).

diarrhea 10 days ago [-]
As a TypeScript beginner, I was bitten by this. Typing in TypeScript feels quite bad for some reason; a lot of effort for effects which are still potentially wrong at runtime. I didn’t struggle so much in Python, Rust or C#, for example. Python is surprisingly… sound? in comparison. It can do nominal as well as structural typing.
lang_agnostic 10 days ago [-]
Isn't this just phantom types in other programming languages?
valcron1000 10 days ago [-]
More like `newtype` with implicit `coerce`
bazoom42 10 days ago [-]
I think Typescript need nominal type aliases for primitives.
klysm 10 days ago [-]
The downside of structural typing
jibbit 10 days ago [-]
or a tagged type if you're old enough to have ever been exposed to another language
mirekrusin 10 days ago [-]
Flow is much better with opaque types.

Also nominal types for classes.

And correct variance.

And adhering to liskov substitution principles.

And exact object types.

And spread on types matching runtime behavior.

And proper no transpilation mode with full access to the language.

And has 10x less LoC than ts.

ps. before somebody says "flow is dead" have a look at flow contributions [0] vs typescript contributions [1]

[0] https://github.com/facebook/flow/graphs/contributors

[1] https://github.com/microsoft/TypeScript/graphs/contributors

recursive 10 days ago [-]
Check out the react codebase, which is presumably the flagship use of flow. It has hundreds of FLOW FIXME annotations. It's easier have a lot of features and small code base if the you don't handle the difficult cases.
mirekrusin 9 days ago [-]
It's the other way around.

Handling difficult cases leads to more type errors.

If type system is lax, it won't flag them but they can fail at runtime.

React has very little of $FlowFixMe annotations for its codebase.

In typescript projects on the other hand it's normal to see unsafety as normal code (casting, non null assertions, implicit and explicit anys etc).

recursive 9 days ago [-]
React vs "typescript projects" doesn't seem very fair. React would presumably be the pinnacle of style with respect to flow. Maybe not true, but it's not just some random project.

Additionally, I've read a lot of typescript code, and the react codebase. My experience is different than yours. I see more type workarounds in react.

FWIW "$FlowFixMe" occurs 822 times in commit hash cf5ab8b8b2c92523aba0b982fb403add053a9110 out of 498900 lines of *.js source. That includes blanks and comments. I don't have any good stats on the occurrence of type assertions in typescript.

mirekrusin 8 days ago [-]
Yes, it's probably not fair.

I think you're double counting it (original files + emitted files)?

    git clone git@github.com:facebook/flow.git
    grep -roh '$FlowFixMe' flow | wc -l
    423
Yes, it's not easy to grab good stats, my experience is that in ts projects you have much more of explicit type annotations vs flow which has better inference, a lot of type casting (unsafe), implicit and explicit anys (unsafe), null assertions (unsafe), unsafe OO etc.

The code in flow has very interesting feel. Missing explicit type annotations are noticeable, feels like js. Optional names in type signature means functions have very Haskell'ish feel:

    const foo /*: number => string */ =
      x =>
        x.toString()
I use flow in comments so I don't have transpilation. Access to full language means a lot here.

Typescript has some nice things that flow doesn't (ie. template literal types) - but recent activity in flow is very interesting. They've put a lot of effort into making flow understand typescript constructs. Many people don't realize how close they now became.

Even some constructs are implemented first in flow (type guards, in ts scheduled for not yet released v5.5 I believe; NoInfer landed first in flow), then in typescript.

It seems they have opportunity to make flow compatible with typescript enough that you could consume ts typedefs from flow - and when it happens it's definitely going to help opening doors wider for adoption.

[0] https://github.com/search?q=repo%3Afacebook%2Fflow+%24FlowFi...

hu3 10 days ago [-]
Only hundreds? Not bad then.

I expected thousands.

vundercind 10 days ago [-]
Tried Flow first, back in the day. I was ready to give up the whole idea as not-at-all-worth-the-trouble if Typescript had been as mediocre an experience.

Fortunately it wasn’t and now I get to not-hate working in JavaScript.

mirekrusin 10 days ago [-]
Yes there was a time when atom/vscode integration was shit.

They also fucked up typings in terms of community management.

Those two alone probably put them into downward spiral.

But the language is being developed with activity stronger than ever.

And it is well designed and pleasure to code in.

nsonha 10 days ago [-]
> adhering to liskov substitution principles

what does this even mean?

> And has 10x less LoC than ts

Prolly b/c Flow isn't able to express the advanced types (albeit with 10x LoC) in the first place.

mirekrusin 10 days ago [-]
You can google, gpt or look at wikipedia for "liskov substitution principles". It's related to object oriented programming, more specifically to inheritance and what is allowed as substitution as superclass vs subclass depending on which position it sits in argument vs return value. It's very interesting read if you don't know about it and you're using OOP.

What advanced types do you have in mind?

ps. the way you're using "albeit" sounds like you think flow has 10x larger codebase, it has 10x smaller codebase

nsonha 10 days ago [-]
I know what SOLID is, questioning how exactly you think Flow has "liskov substitution" as a feature.
mirekrusin 10 days ago [-]
L in SOLID refers to Liskov substitution principles.

It's the only one from the SOLID list which can be typechecked - others are design principles.

"Flow adhering to it" means that violating code will be flagged by type system.

It matters because unlike other principles, violations can cause runtime errors.

nsonha 9 days ago [-]
> L in SOLID refers to Liskov substitution

you think?

> Flow adhering to it" means that violating code will be flagged by type system

Shocking that type system can flag things

eyelidlessness 10 days ago [-]
FWIW, classes in TypeScript become nominal types when they have a non-public member. But I definitely do feel real FOMO over Flow’s opaque types.
mirekrusin 10 days ago [-]
Yes, ts is full of this kind kind of adhoc-ifs-like glued together, also exactness check only when it's literal object etc.

Flow is more principled.

eyelidlessness 10 days ago [-]
I mean. I think this specific case is a lot more principled than you seem to think. It’s certainly well reasoned from a perspective of structural typing by default.
mirekrusin 10 days ago [-]
Yes, they have their reasons. They always do, tradeoff etc, I know.

It doesn't change the fact that ie. adding private member is breaking change in your library which is kind of funny (until it's not funny of course).

Also stuff like:

    class Foo { private foo = 1 }
    class Bar {}
    const a: Bar = new Foo
...typechecks so that's it for nominality.

It's all ifs all the way down.

eyelidlessness 10 days ago [-]
It’s only a breaking change if you’ve been using a class to stand in for an interface (happens to the best of us I’m sure!). You can still avoid it being one after the fact by introducing a non-breaking interface consistent with what you were already accepting/expecting.

And yeah, I’m not a fan of that class instance assignability case. Not to make excuses for it, but I have other reasons I generally prefer to expose interfaces (distinct from classes, even if they’re identical in shape and even have the same internal purpose) at most API boundaries; avoiding that particular footgun just turns out to be a nice happy side effect of the preference.

yen223 9 days ago [-]
Flow regularly crashed with bizarre segfaults errors, the last time I used it.
mirekrusin 8 days ago [-]
Sounds like vscode integration few years ago, yes.
baw-bag 10 days ago [-]
flow is dead (for jobs). I'm gonna annihilate Dart (apart from jobs)
mirekrusin 10 days ago [-]
java is great for jobs as well.
10 days ago [-]
pprotas 10 days ago [-]
Amazing, we have invented nominal typing in a structural typing system.

Sometimes I wonder what kind of programs are written using all these complicated TS types. Anecdotally, we use very simple, basic types in our codebase. The need for tricks like this (and other complicated types) is simply not there. Are we doing something wrong?

JJMalina 10 days ago [-]
I think the example could be better in this article. Let's say you have a function that takes a database id, an email address, and a name. They're all strings. If you pass arguments to this function and you mess up the order for some reason then the compiler has no idea. Hopefully your unit tests catch this but it won't be as obvious. Branded types solve this problem because you can't pass a name string as an argument that is expected to be an email address.

If you argue that this is not a common problem in practice, then I tend to agree. I haven't seen code with mixed up arguments very much, but when it does happen it can have bad consequences. IMO there is not a big cost to pay to have this extra safety. In TypeScript it's ugly, but in other languages that natively support this typing like Scala, Rust, Swift and Haskell, it works nicely.

contextnavidad 10 days ago [-]
> because you can't pass a name string as an argument that is expected to be an email address

Unless you accidentally create the wrong branded type? Which is as likely as disordered arguments.

As you stated, tests should cover this case trivially, I don't see the value in added type complexity.

LadyCailin 10 days ago [-]
That’s just plain encapsulation, if I understand you correctly. Branding, on the other hand, prevents complex types from being confused.
ebolyen 10 days ago [-]
Branding works on primitive types as well, which is I think the most interesting use case.

I would also agree that it's harder to confuse complex types as any single instance of a type is unlikely to overlap once you have a few fields.

mason55 10 days ago [-]
Not really, not for TS at least. If you just want to take a string and call it an email address and have your function only accept email addresses (the simplest use case) then you need to use branding. That's not encapsulation.
antifa 9 days ago [-]
Or double your GC burden by using a wrapper class.
chowells 10 days ago [-]
You've never had values that are only valid within a specific context and wanted to prevent using them incorrectly?
IshKebab 10 days ago [-]
Yeah structural typing assumes that all fields with the same name & type mean the same thing, but that clearly isn't always the case.
skybrian 10 days ago [-]
I use branded types for database id’s to avoid mixing them up. It seems like better documentation and might catch some errors, but mixing up database id’s seems like an uncommon error, so hard to say.
brigadier132 10 days ago [-]
You don't use them but your favorite libraries do.
maeil 10 days ago [-]
This is very accurate, in particular when the output type of something depends on the input type.

Data validation, typed database access, or functional programming libraries are good examples. Particularly the modern, leading libraries of such areas, if you look into their code you'll generally see very intricate typing. For FP libraries it's particularly tough. I like to use Remeda which emphasizes being very type-safe, but that means it's inherently more limited in what functions it can offer compare to other libraries which choose to compromise their type-safety. These kinds of techniques mean that libraries can offer greater functionality while remaining type-safe.

eyelidlessness 10 days ago [-]
Not necessarily wrong. You’re probably doing the same work at runtime. That might be either just as good (a subjective preference) or more correct (for certain kinds of dynamism) depending on context. In some cases, there are clear static invariants that could be guaranteed at compile time, and some folks will be more inclined to squeeze as much out of that as possible. In my experience, the benefits of that vary wildly from “total waste of time” to “solves a recurring and expensive problem for good”, with a lot in between.
shadowgovt 10 days ago [-]
Probably not.

I could definitely see using something like this to force some constraints on my favorite bugbear for my problem domain: values that should have units. It matters a lot if "time" is nanoseconds, microseconds, or seconds, but most time-related functions just take in "number" like that's okay and won't cause thousand- or million-fold magnitude errors.

This is one way to provide some language safeguarding against treating 5 nanoseconds as the same as 5 microseconds.

CapsAdmin 10 days ago [-]
> The need for tricks like this (and other complicated types) is simply not there.

It just depends on how constrained by types you want your code to be and how much time and effort you're willing to spend maintaining and writing code that fits within those constraints.

Sometimes complicated types are introduced because you want to maintain editor features such as find by reference and the ability to refactor later. When it works, removing or adding new features feels fast, easy and safe.

In the articles case you could differentiate between an email string type and a user id string type. Maybe sometime in the future you want to change the id to an integer instead, so now that it's already distinguished you could find all those places where that's applied.

That's at least a selling point, in practice I've used this a few times but it doesn't come up that often. Sometimes the blast radius of a type isn't big so it's not worth doing.

odyssey7 10 days ago [-]
Nope, if you have lower expectations for your type system and are using TS then you are properly where you want to be.
amatecha 10 days ago [-]
Yeah no, IMO this seems totally extraneous and a layer of complexity not worth introducing to any project. I've never encountered a case where I care that I need to differentiate between two types of exactly the same structure, and my gut feeling (without actually prototyping out an example) is that if this actually makes a difference in your code, you should probably be making the differentiation further up in the flow of information...
Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact
Rendered at 17:12:09 GMT+0000 (Coordinated Universal Time) with Vercel.