Live data from Hacker News

Adding type safety to object IDs in TypeScript

kravchyk.com

41–50 of 61 posts

Re: Adding type safety to object IDs in TypeScript

#41

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?

Re: Adding type safety to object IDs in TypeScript

#42

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.

I've done it in Kotlin, and I suspect that modern Java should be quite amenable to it with records. It was really nice in my Kotlin project because we were dealing with legacy data structures with very confusing names—being able to guarantee that a UserID doesn't accidentally get passed where a UserDataID was expected helped prevent a lot of the bugs that plagued the legacy apps.

That's great to hear. We did the same, but in C#, using its records. The codebase didn't exactly suffer from errors from ID misuse (all of which were the same type beforehand), but it's great for future-proofing as well.

Added benefit, as always when leaning into the type system more, is a reduction in the number of unit tests required. The need to test that `update_user(group_id)` fails (because a non-user ID was passed) simply disappears.

Re: Adding type safety to object IDs in TypeScript

#43
post #12
post #5

This is pretty close to type branding (newtype wrapping for the Haskell-inclined), though using template literal types is pretty novel. Normal brands look something like this: type Brand = BaseType & { readonly __brand__: Brand }; type FooId = Brand ; function fooBar(asdf: FooId | 'foobar'): void { } fooBar will only accept the literal string 'foobar' or a true FooId, but not any arbitrary string. FooId would then co…

The easiest way I know of is declare const isMyID: unique symbol; export type MyID = string & { [isMyID]: true };

nice!

Re: Adding type safety to object IDs in TypeScript

#44
post #21

Earlier quoted context omitted.

What is the problem with the workaround suggested in the last comment there?

You shouldn't need to write this kind of thing manually for every such type.

    function is(value: string, prefix: T): value is `${typeof prefix}_${string}` {
      return value.startsWith(`${prefix}_`)
    }

You can now do `is(id, 'user')`.

If you do that often you probably want to create separate functions, e.g.:

    function isFactory(prefix: T) {
      return (value: string) => is(value, prefix)
    }

    const isUser = isFactory('user')
    const isOrder = isFactory('order')
Not too bad.

Re: Adding type safety to object IDs in TypeScript

#45
post #28
post #16

Its bit sad that startsWith doesn't narrow the type, making this pattern slightly less convenient. The GH issue: https://github.com/microsoft/TypeScript/issues/46958

I wish that TS had better type narrowing for the JS standard library, though there's a lot of constraints and design limitations that make it impractical. I ran into a similar issue with the some() method on Array not narrowing types a while back [1]; that issue links to the same sort of issue with filter(), as well as issues where the TS team has discussed what they can and can't do in control flow analysis. [1] htt…

you can improve it a bit with the library ts-reset

https://github.com/total-typescript/ts-reset

Re: Adding type safety to object IDs in TypeScript

#46
post #38

Earlier quoted context omitted.

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.

Sure, the TS type system is not sound but the idea is not to stop "bad guys", it's to help you realize you are doing something unintended.

Agreed, for instance in our codebase we just make all type assertions a lint error demanding a justification, as well as flat out banning the any type. But anyone is free to write shoddy TypeScript.

Re: Adding type safety to object IDs in TypeScript

#47

Earlier quoted context omitted.

This solution only works with strings, whereas branded types can be used with numbers as well, or any kind of object that you want to add stricter types to without modifying the runtime value. I haven't observed any issues with branded types and infer—is there documentation somewhere about the problem?

As others pointed out, TypeScript sometimes reasons `string & object` or similar as an impossible type and can turn it into `never` at any time. I don't exactly recall whether `infer` triggered that or it was a separate issue, but that was a major problem in my experience.

FWIW I’ve been using branded types for years and never had this issue.

Re: Adding type safety to object IDs in TypeScript

#48
post #28

Earlier quoted context omitted.

I wish that TS had better type narrowing for the JS standard library, though there's a lot of constraints and design limitations that make it impractical. I ran into a similar issue with the some() method on Array not narrowing types a while back [1]; that issue links to the same sort of issue with filter(), as well as issues where the TS team has discussed what they can and can't do in control flow analysis. [1] htt…

you can improve it a bit with the library ts-reset https://github.com/total-typescript/ts-reset

And also can declare your own wrappers to at least achieve it for your own codebase.

Re: Adding type safety to object IDs in TypeScript

#49

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 param, are less likely to have overlap with keys from other types (even more so if you prefix the key with a type identifier), no need to know that last ID used in the DB so you can build your key in app code instead of the DB, and I’m sure I have more things I like about them.

I understand auto-inc can have some performance gains in the DB but I’ve never needed the gains more than I wanted sane (in my mind) ids.

For the longest time I used UUID (v4) and I still do sometimes but lately I’ve liked KSUID since they are sortable by create date (great for things like DynamoDB IMHO).

Re: Adding type safety to object IDs in TypeScript

#50
post #2

Stripe does this (prefix ids with object type). Its smart. Makes it much easier to work with the ids.

Agreed, I can look at any ID and know what type it is. Even with well named fields it helps a ton in the docs “oh, they pass an account in or a payment ID”. Even in my own DB if I reference the stripe ID I know what it is without even having to look at the column name.
Post reply on HN