Hacker News new | past | comments | ask | show | jobs | submit login

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.



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.


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?


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.


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


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


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


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


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.


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.


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.


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


> 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!


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.




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: