TE
TechEcho
Home24h TopNewestBestAskShowJobs
GitHubTwitter
Home

TechEcho

A tech news platform built with Next.js, providing global tech news and discussions.

GitHubTwitter

Home

HomeNewestBestAskShowJobs

Resources

HackerNews APIOriginal HackerNewsNext.js

© 2025 TechEcho. All rights reserved.

Adding type safety to object IDs in TypeScript

144 pointsby mckravchykover 1 year ago

17 comments

headbeeover 1 year ago
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&lt;BaseType, Brand&gt; = BaseType &amp; { readonly __brand__: Brand }; type FooId = Brand&lt;string, &#x27;FooId&#x27;&gt;; function fooBar(asdf: FooId | &#x27;foobar&#x27;): void { } </code></pre> fooBar will only accept the literal string &#x27;foobar&#x27; 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
评论 #39195959 未加载
评论 #39196607 未加载
评论 #39196673 未加载
评论 #39195888 未加载
评论 #39199711 未加载
评论 #39195732 未加载
DanHultonover 1 year ago
If you want a type-prefixed UUIDv7 type, I can wholeheartedly recommend TypeID-JS: <a href="https:&#x2F;&#x2F;github.com&#x2F;jetpack-io&#x2F;typeid-js">https:&#x2F;&#x2F;github.com&#x2F;jetpack-io&#x2F;typeid-js</a><p>Also available for a whole bunch of other languages: <a href="https:&#x2F;&#x2F;github.com&#x2F;jetpack-io&#x2F;typeid">https:&#x2F;&#x2F;github.com&#x2F;jetpack-io&#x2F;typeid</a><p>UUIDv7 is UUIDv4-compatible (i.e. you can put a v7 UUID anywhere a v4 UUID would go, like in Postgres&#x27;s UUID datatype) and is time-series sortable, so you don&#x27;t lose that nice lil&#x27; 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 = &#x27;user&#x27;; export type UserTypeID = TypeID&lt;&#x27;user&#x27;&gt;; 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: &#x27;${tid.toString()}&#x27;.`, ); return tid.toUUID(); } } @Entity() export class User { @PrimaryColumn({ type: &#x27;uuid&#x27;, primaryKeyConstraintName: &#x27;user_pkey&#x27;, transformer: new UserIdTransformer(), }) id: UserTypeID; @BeforeInsert() createNewPrimaryKey() { this.id = typeid(TYPEID_USER); } }</code></pre>
zokierover 1 year ago
Its bit sad that startsWith doesn&#x27;t narrow the type, making this pattern slightly less convenient. The GH issue: <a href="https:&#x2F;&#x2F;github.com&#x2F;microsoft&#x2F;TypeScript&#x2F;issues&#x2F;46958">https:&#x2F;&#x2F;github.com&#x2F;microsoft&#x2F;TypeScript&#x2F;issues&#x2F;46958</a>
评论 #39198335 未加载
评论 #39196995 未加载
sisveover 1 year ago
Stripe does this (prefix ids with object type). Its smart. Makes it much easier to work with the ids.
评论 #39203357 未加载
shermantanktopover 1 year ago
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).
评论 #39200649 未加载
评论 #39203339 未加载
matthewfcarlsonover 1 year ago
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
sophiabitsover 1 year ago
Type-prefixed IDs are the way to go. For completeness it&#x27;s worth noting that the first example using the `string | &#x27;currentNode&#x27;` 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 = &#x27;currentNode&#x27; | (string &amp; {}); const targets: Target[] = [ &#x27;currentNode&#x27;, &#x2F;&#x2F; you get autocomplete hints for this! &#x27;somethingElse&#x27;, &#x2F;&#x2F; no autocomplete here, but it typechecks ];</code></pre>
评论 #39197657 未加载
seanwilsonover 1 year ago
I&#x27;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&#x27;s several of these around and you&#x27;re subtracting&#x2F;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&#x27;s always other variables around you could get them mixed up with.
tinrabover 1 year ago
I like using &quot;resource names&quot; defined by Google&#x27;s AIP (<a href="https:&#x2F;&#x2F;google.aip.dev&#x2F;122" rel="nofollow">https:&#x2F;&#x2F;google.aip.dev&#x2F;122</a>). For example, the name &quot;users&#x2F;1&#x2F;projects&#x2F;42&quot; is a nested project resource of a user &quot;users&#x2F;1&quot;. TypeScript type could be &quot;users&#x2F;${number}&quot;.
评论 #39207106 未加载
rcktover 1 year ago
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.
评论 #39197244 未加载
mckravchykover 1 year ago
I did not know about type branding, but it would also be possible to just use casting if you don&#x27;t want the prefix at runtime:<p><pre><code> type UserId = `usr_${string}` const user = { id: &#x27;bgy5D4eL&#x27; 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 &#x2F;front when it initially did not get any upovtes. I&#x27;m curious how it works :)
jakubmazanecover 1 year ago
Related: <a href="https:&#x2F;&#x2F;github.com&#x2F;sindresorhus&#x2F;type-fest&#x2F;blob&#x2F;main&#x2F;source&#x2F;opaque.d.ts">https:&#x2F;&#x2F;github.com&#x2F;sindresorhus&#x2F;type-fest&#x2F;blob&#x2F;main&#x2F;source&#x2F;o...</a>
hallman76over 1 year ago
Has anyone tried using custom types for ids in java?<p>I considered doing it on a recent project, but it doesn&#x27;t seem very common so I was reluctant to introduce it.
评论 #39199906 未加载
评论 #39214468 未加载
评论 #39199229 未加载
sagiaover 1 year ago
I wonder if a similar (but maybe more bloated) implementation using interfaces (and probably generics too?) will work in this case.
评论 #39196654 未加载
jannesover 1 year ago
Reminds me of Slack IDs:<p>- Channel IDs always start with C<p>- User IDs always start with U
评论 #39200046 未加载
snowstormsunover 1 year ago
Nice!
ken47over 1 year ago
Typescript is a work of art created on a barren canvas. Kudos to the team behind this language.