Live data from Hacker News

Adding type safety to object IDs in TypeScript

kravchyk.com

31–40 of 61 posts

Re: Adding type safety to object IDs in TypeScript

#31

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. There’s little cost, but it does feel unidiomatic in Java for some reason.

Not sure whether I’d do it again or not. It’s hard to say how much benefit that I’m getting out of them.

Re: Adding type safety to object IDs in TypeScript

#32
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…

In my experience branded types are relatively more fragile than normal types though. IIRC they badly behaved with infer types in particular, and it was quite hard to work around. This solution seems more versatile. (Of course, I want to see a built-in branded type support in TS as well.)

Re: Adding type safety to object IDs in TypeScript

#33
If you want a type-prefixed UUIDv7 type, I can wholeheartedly recommend TypeID-JS: https://github.com/jetpack-io/typeid-js

Also available for a whole bunch of other languages: https://github.com/jetpack-io/typeid

UUIDv7 is UUIDv4-compatible (i.e. you can put a v7 UUID anywhere a v4 UUID would go, like in Postgres's UUID datatype) and is time-series sortable, so you don't lose that nice lil' benefit of auto-incrementing IDs.

And if you use something like TypeORM to define your entities, you can use a Transformer to save as plain UUIDv7 in the DB (so you can use UUID datatypes, not strings), but deal with them as type-prefixed strings everywhere else:

    export const TYPEID_USER = 'user';

    export type UserTypeID = TypeID;
    
    export type UserTypeString = `user_${string}`;
    
    export class UserIdTransformer implements ValueTransformer {
      from(uuid: string): UserTypeID {
        return TypeID.fromUUID(TYPEID_USER, uuid);
      }
    
      to(tid: UserTypeID): string {
        assert.equal(
          tid.getType(),
          TYPEID_USER,
          `Invalid user ID: '${tid.toString()}'.`,
        );
    
        return tid.toUUID();
      }
    }
    
    @Entity()
    export class User {
      @PrimaryColumn({
        type: 'uuid',
        primaryKeyConstraintName: 'user_pkey',
        transformer: new UserIdTransformer(),
      })
      id: UserTypeID;
    
      @BeforeInsert()
      createNewPrimaryKey() {
        this.id = typeid(TYPEID_USER);
      }
    }

Re: Adding type safety to object IDs in TypeScript

#34
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…

In my experience branded types are relatively more fragile than normal types though. IIRC they badly behaved with infer types in particular, and it was quite hard to work around. This solution seems more versatile. (Of course, I want to see a built-in branded type support in TS as well.)

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?

Re: Adding type safety to object IDs in TypeScript

#35

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.

Re: Adding type safety to object IDs in TypeScript

#38
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…

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.

Re: Adding type safety to object IDs in TypeScript

#39

Earlier quoted context omitted.

In my experience branded types are relatively more fragile than normal types though. IIRC they badly behaved with infer types in particular, and it was quite hard to work around. This solution seems more versatile. (Of course, I want to see a built-in branded type support in TS as well.)

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.

Re: Adding type safety to object IDs in TypeScript

#40
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.

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.
Post reply on HN