Live data from Hacker News

Adding type safety to object IDs in TypeScript

kravchyk.com

51–60 of 61 posts

Re: Adding type safety to object IDs in TypeScript

#51
I've done something similar for URLs (stops you mixing up whole URLs, substrings of URLs and regular strings), relative vs absolute time (easy to mix these up when there's several of these around and you're subtracting/adding to create new times) and color spaces (stops you mixing up tuples of RGB and HSL values). Feels very worthwhile for object IDs as well as there's always other variables around you could get them mixed up with.

Re: Adding type safety to object IDs in TypeScript

#52
I did not know about type branding, but it would also be possible to just use casting if you don't want the prefix at runtime:

  type UserId = `usr_${string}`
  const user = { id: 'bgy5D4eL' as unknown as UserId }
Casting would just need to be applied wherever the object is generated, retrieving from the database requires casting either way. It could be a footgun though, if someone working on the codebase thought that the prefix is actually there and decided to use it for a runtime check.

I wanted to add this to the article, but decided not to, since I think having the prefix at runtime is just as useful - wherever the ID occurs, i.e. in error log the type of the object is always clear. But that or type branding is something that is much easier to apply in an existing system indeed.

Btw. I submitted this on Monday 6 AM EST and now it is visible as submitted 19h ago? I totally did not expect to make it far, let alone /front when it initially did not get any upovtes. I'm curious how it works :)

Re: Adding type safety to object IDs in TypeScript

#53
post #38

Earlier quoted context omitted.

If you want to make this easier to keep private between encapsulation boundaries the additional suggestion is make sure the Brand type extends symbol: type Brand = BaseType & { readonly __brand__: Brand }; const FooIdBrand = Symbol('FooId'); type FooId = Brand ; function fooBar(asdf: FooId | 'foobar'): void { } Using a private shared symbol your authoritative validation/sources can share your brand symbol and no one…

Unfortunately this doesn’t work, at least not from a type safety perspective, because even without access to the symbol, nothing stops anyone from doing `let myFooId = 'foo' as any as FooId;`. You could detect this at runtime, but type safety is compile time.

Right, hence "closest to" in my description. Typescript's role ends at compile time and it can't/won't stop bad actors at runtime. Typescript tries to make it easier for good actors to do the right thing more of the time.

That said, the other benefit to using private symbols like this is that they are also easy to enforce at runtime, because symbol visibility is enforced at runtime (you can't create the same signal by hand somewhere else). It can be as easy as something like:

    console.assert(id.__brand__ === FooIdBrand)
(That still won't stop the determined hacker in the console dev tools, if they can see a symbol they can create a reference to it, defense in depth will always be a thing.)

Re: Adding type safety to object IDs in TypeScript

#54

I’ve done this in multiple languages. I dislike libraries that return string ids. The proliferation of string identifiers is a pet peeve of mine. It’s what I call “stringly typed” code (not my coinage but I use it all the time).

"Stringly typed", the way I've heard it, is a valid criticism when people replace type safety with magic strings which may or may not be checked at runtime but certainly not at compile time. However, that's not the case when it comes to Typescript, because literal and union string types are actually checked at compile time. So what is the problem?

If everything is a string, you can accidentally use a UserID as a PostID.

Re: Adding type safety to object IDs in TypeScript

#55
post #27

I like using "resource names" defined by Google's AIP ( https://google.aip.dev/122 ). For example, the name "users/1/projects/42" is a nested project resource of a user "users/1". TypeScript type could be "users/${number}".

Because they use slashes, those are kinda annoying to use in web UIs.

Say, /view/users/jdoe/foo -- is that foo a resource, or a URL my web framework can use to e.g. fetch data & components SPA style?

With a flat /view/user_jdoe/foo, you don't have that source of confusion.

Re: Adding type safety to object IDs in TypeScript

#56

Has anyone tried using custom types for ids in java? I considered doing it on a recent project, but it doesn't seem very common so I was reluctant to introduce it.

In Scala it worked great using AnyVal wrappers around primitive types. It’s something I miss in typescript, where type aliases are more for documentation purposes on id types but don’t add much type safety. I think they trick is the type needs value semantics, which records should help with.

Re: Adding type safety to object IDs in TypeScript

#57

Earlier quoted context omitted.

"Stringly typed", the way I've heard it, is a valid criticism when people replace type safety with magic strings which may or may not be checked at runtime but certainly not at compile time. However, that's not the case when it comes to Typescript, because literal and union string types are actually checked at compile time. So what is the problem?

If everything is a string, you can accidentally use a UserID as a PostID.

Exactly. Especially easy if the variable name is “id”. For API methods which take multiple ids, the order is easy to mix up - though in TS that is conventionally handled with options objects.

Re: Adding type safety to object IDs in TypeScript

#58

I’ve done this in multiple languages. I dislike libraries that return string ids. The proliferation of string identifiers is a pet peeve of mine. It’s what I call “stringly typed” code (not my coinage but I use it all the time).

What’s your aversion to string ids? Personally I love them and prefer them in all cases. They aren’t enumerable, never get confused for “is this an array or a map by ID” in PHP, can be used safely as keys without some languages (looking at you PHP) returning an array instead of an object (assoc. array) when converting to JSON, don’t need to be converted back to a number after passing through something like a URL/get…

Identifiers belong to a domain. A userid belongs to a member of a set of Users, a groupid to Groups, and so on. You are happy to have a User object with a distinct type, and wouldn’t try to have a superset UserOrGroup class.

A string belongs to the domain of “all strings” and so the type system and compiler cannot catch something like “authorizeUser(id,…) where the id is in fact “” or a groupid or “null” or “undefined”.

Lots of code does what you describe. But I prefer to use the type system wherever I can.

Re: Adding type safety to object IDs in TypeScript

#59

Earlier quoted context omitted.

If everything is a string, you can accidentally use a UserID as a PostID.

Exactly. Especially easy if the variable name is “id”. For API methods which take multiple ids, the order is easy to mix up - though in TS that is conventionally handled with options objects.

But this is exactly what the article is about? I don't know what you mean by "option object" but it doesn't sound any more conventional than union types to me.

Re: Adding type safety to object IDs in TypeScript

#60

Earlier quoted context omitted.

Exactly. Especially easy if the variable name is “id”. For API methods which take multiple ids, the order is easy to mix up - though in TS that is conventionally handled with options objects.

But this is exactly what the article is about? I don't know what you mean by "option object" but it doesn't sound any more conventional than union types to me.

What I meant by option objects is a dictionary object holding all parameters.

doStuff({userId: foo, itemId: bar});

This allows the order of key/val pairs to move around, making it more robust to mistakes than doStuff: (string,string) => void.

Post reply on HN