[HN Gopher] Adding type safety to object IDs in TypeScript
___________________________________________________________________
Adding type safety to object IDs in TypeScript
Author : mckravchyk
Score : 50 points
Date : 2024-01-29 11:04 UTC (1 days ago)
(HTM) web link (www.kravchyk.com)
(TXT) w3m dump (www.kravchyk.com)
| sisve wrote:
| Stripe does this (prefix ids with object type). Its smart. Makes
| it much easier to work with the ids.
| sagia wrote:
| I wonder if a similar (but maybe more bloated) implementation
| using interfaces (and probably generics too?) will work in this
| case.
| jeroenhd wrote:
| Something like this, maybe?
|
| https://www.typescriptlang.org/play?#code/C4TwDgpgBAkgIlAvFA...
|
| Alternatively, with generics:
| https://www.typescriptlang.org/play?#code/JYOwLgpgTgZghgYwgA...
|
| I don't think these are better, to be honest. The string types
| suffice and are easier to exchange with servers and other APIs.
| matthewfcarlson wrote:
| I love this approach but I augment it with Zod branded types. IDs
| have a known start to their identifier and anything coming in and
| out of the database is verified match a schema
| headbee wrote:
| 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, Brand> = BaseType & { readonly __brand__:
| Brand }; type FooId = Brand<string, 'FooId'>;
| 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 come from a
| function that validates strings as FooIds, or some other part of
| the app that is an authoritative source for them. Brands extend
| their BaseType so they can be used anywhere their BaseType is
| used, but not the inverse
| ssalka wrote:
| I prefer the Brand solution, it works well for existing ID sets
| that you can't easily migrate to have an actual string prefix
| throwanem wrote:
| Also for values that aren't strings at all.
| blue_pants wrote:
| The meaning is different though. Brands convey intent, UserId
| brand would allow only other UserId brands, but with string
| literal types "any" type that matches 'user_${string}' will do
| WorldMaker wrote:
| 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, Brand extends symbol> = BaseType & { readonly
| __brand__: Brand }; const FooIdBrand = Symbol('FooId');
| type FooId = Brand<string, typeof FooIdBrand>; function
| fooBar(asdf: FooId | 'foobar'): void { }
|
| Using a private shared symbol your authoritative
| validation/sources can share your brand symbol and no one else
| can create one without using your validation. Private symbol
| brands in this way become the closest Typescript gets to
| "nominal types".
| Jasper_ wrote:
| The easiest way I know of is declare const
| isMyID: unique symbol; export type MyID = string & {
| [isMyID]: true };
| lf-non wrote:
| It is also convenient to use a unique symbol for the brand
| (declare const brand: unique symbol). Then we can combine
| multiple brands in the same type and if we don't export that
| symbol type, we simply dont have a way to access the brand
| property at runtime.
| snowstormsun wrote:
| Nice!
| jannes wrote:
| Reminds me of Slack IDs:
|
| - Channel IDs always start with C
|
| - User IDs always start with U
| jakubmazanec wrote:
| Related: https://github.com/sindresorhus/type-
| fest/blob/main/source/o...
| zokier wrote:
| 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
| mattigames wrote:
| What is the problem with the workaround suggested in the last
| comment there?
| rckt wrote:
| Not sure if it's a valid point, but what I would like to have -
| kind of a regex or a template for strings or numbers. Otherwise,
| it's still just a sting or a specific value. It's not like you
| are free to update backend to prefix ids to your liking. Most of
| the time you have to work with set schemas.
___________________________________________________________________
(page generated 2024-01-30 23:00 UTC)