Live data from Hacker News

TypeScript: Branded Types

prosopo.io

171–180 of 209 posts

Re: TypeScript: Branded Types

#171
post #158

Earlier quoted context omitted.

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

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

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

Re: TypeScript: Branded Types

#172

Earlier quoted context omitted.

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.

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

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.

Re: TypeScript: Branded Types

#173
post #158

Earlier quoted context omitted.

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

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

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.

Re: TypeScript: Branded Types

#175
post #141

Earlier quoted context omitted.

Is all Typescript unreadable, or is that just your style?

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?

this is readable?

    export type Brand = 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.

Re: TypeScript: Branded Types

#176
post #166
post #72

Earlier quoted context omitted.

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

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.

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.

Re: TypeScript: Branded Types

#177
post #166
post #72

Earlier quoted context omitted.

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

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.

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.

Re: TypeScript: Branded Types

#178
post #40

Earlier quoted context omitted.

That’s just plain encapsulation, if I understand you correctly. Branding, on the other hand, prevents complex types from being confused.

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.

Or double your GC burden by using a wrapper class.

Re: TypeScript: Branded Types

#179
post #26

Earlier quoted context omitted.

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; }

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.

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.

Re: TypeScript: Branded Types

#180
post #157

Earlier quoted context omitted.

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

Are these simple aliases or nominally distinct types?

Nominally distinct.

https://go.dev/play/p/4am-a1i4_ox

Post reply on HN