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:<p><pre><code> type Brand<BaseType, Brand> = BaseType & { readonly __brand__: Brand };
type FooId = Brand<string, 'FooId'>;
function fooBar(asdf: FooId | 'foobar'): void { }
</code></pre>
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
If you want a type-prefixed UUIDv7 type, I can wholeheartedly recommend TypeID-JS: <a href="https://github.com/jetpack-io/typeid-js">https://github.com/jetpack-io/typeid-js</a><p>Also available for a whole bunch of other languages: <a href="https://github.com/jetpack-io/typeid">https://github.com/jetpack-io/typeid</a><p>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.<p>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:<p><pre><code> export const TYPEID_USER = 'user';
export type UserTypeID = TypeID<'user'>;
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);
}
}</code></pre>
Its bit sad that startsWith doesn't narrow the type, making this pattern slightly less convenient. The GH issue: <a href="https://github.com/microsoft/TypeScript/issues/46958">https://github.com/microsoft/TypeScript/issues/46958</a>
I’ve done this in multiple languages. I dislike libraries that return string ids.<p>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).
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
Type-prefixed IDs are the way to go. For completeness it's worth noting that the first example using the `string | 'currentNode'` type can be slightly improved in cases where you _do_ want autocomplete for known-good values but are still OK with accepting arbitrary string values:<p><pre><code> type Target = 'currentNode' | (string & {});
const targets: Target[] = [
'currentNode', // you get autocomplete hints for this!
'somethingElse', // no autocomplete here, but it typechecks
];</code></pre>
I've done something similar for URLs (stops you mixing up whole URLs, substrings of URLs and regular strings), relative vs absolute time (easy to mix these up when there's several of these around and you're subtracting/adding to create new times) and color spaces (stops you mixing up tuples of RGB and HSL values). Feels very worthwhile for object IDs as well as there's always other variables around you could get them mixed up with.
I like using "resource names" defined by Google's AIP (<a href="https://google.aip.dev/122" rel="nofollow">https://google.aip.dev/122</a>). For example, the name "users/1/projects/42" is a nested project resource of a user "users/1". TypeScript type could be "users/${number}".
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.
I did not know about type branding, but it would also be possible to just use casting if you don't want the prefix at runtime:<p><pre><code> type UserId = `usr_${string}`
const user = { id: 'bgy5D4eL' as unknown as UserId }
</code></pre>
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.<p>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.<p>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 :)
Has anyone tried using custom types for ids in java?<p>I considered doing it on a recent project, but it doesn't seem very common so I was reluctant to introduce it.