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