Adding type safety to object IDs in TypeScript
51–60 of 61 posts
Re: Adding type safety to object IDs in TypeScript
#52 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
#53Earlier 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.
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
#54I’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?
Re: Adding type safety to object IDs in TypeScript
#55I 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}".
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
#56Has 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.
Re: Adding type safety to object IDs in TypeScript
#57Earlier 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.
Re: Adding type safety to object IDs in TypeScript
#58I’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…
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
#59Earlier 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.
Re: Adding type safety to object IDs in TypeScript
#60Earlier 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.
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.