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?
Require type annotations in certain places. TypeScript cannot always infer types for all places in code. Some locations require type annotations for their types to be inferred. This rule can enforce type annotations in locations regardless of whether they're required.
Overview. The @typedef tag is useful for documenting custom types, particularly if you wish to refer to them repeatedly. These types can then be used within other tags expecting a type, such as @type or @param. Use the @callback tag to document the type of callback functions.
Using ?: with undefined as type definition While there are no errors with this interface definition, it is inferred the property value could undefined without explicitly defining the property type as undefined . In case the middleName property doesn't get a value, by default, its value will be undefined .
// That said, we recommend you use interfaces over type aliases. Specifically, because you will get better error messages. If you hover over the following errors, you can see how TypeScript can provide terser and more focused messages when working with interfaces like Chicken.
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.
You cannot do that in TypeScript.
Using aliases does not prevent from passing values of other type. If you declare type B as alias of type A, in all places where type B is expected you can pass also parameters of type A and vice versa.
What about encapsulating one type into another? Suppose types A and B both have a single member of type number. Will such wrapper help? No. The compatibility of classes and interfaces in TypeScript is determined based on their structure. If two different types have identical structure they are compatible. It is possible to use one instead of another.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With