I have various types of integer IDs in my app (e.g. ProductId, UserId etc.) which I want to implement strong typing so that I can be sure I am passing the correct ID type to methods.
e.g. I want to declare GetProduct(productId: ProductId)
instead of GetProduct(productId: number)
such that only ProductId typed variables can be passed to it.
In my C days, I would use a typedef - e.g. typedef ProductId int;
In C#, I accomplished this by defining a ProductId class with an implicit cast to int operator and an explicit cast from int operator. More cumbersome than a typedef, but it works.
I'm trying to figure out how to do the equivalent in TypeScript. For TypeScript, I tried this:
export class ProductId extends Number {}
but this still allows a number to be passed in place of a ProductId.
How would one accomplish this in TypeScript?
It is possible to do this, with some effort.
The trick is to define your ProductID
as something that is in TS different from number, but still a number when actually running as Javascript.
I wrote about this on my blog here: https://evertpot.com/opaque-ts-types/
But I will share the important details here:
declare const validProductId: unique symbol;
type ProductId = number & {
[validProductId]: true
}
Note that even though we declared a 'unique symbol', this is completely stripped from Javascript, so there's not actually a symbol added to your ProductId
, which would be a pain.
To actually get a something recognized as a ProductId
, you will need to write either an assertion function, a type guard function or just cast from a place where ProductId
's are actually allowed to be generated.
And just to repeat, there is no actual need to have add this symbol to your ProductId
, this is just to make sure that Typescript recognizes ProductId
as a distinct type from number
. During runtime, it's just a number
.
This is a great pattern for your use-case. It's basically a marker that this is not just any number, it's specifically a number that has been vetted by your business logic as a product id.