We would believe that TypeScript would prevent us pass a value of type PostId when UserId is expected.
Unfortunately, both userId & postId can be used interchangeably. For the compiler, the names are irrelevant, as they are an alias to the same string type.
TypeScript doesn’t care how we name our values, only if the shape they describe (e.g string) can satisfy the constraints.
TypeScript uses structural typing. Which is a bit different from what you might have used in Java.
In our case TypeScript doesn’t care if a type has an explicit inheritance from another. If its contents are equal (or a superset) it’s fine. Here’s another example:
For TypeScript both Student & Teacher are equivalent. As long as the contents are the same, any implementation that uses a Studentvalue, can be also satisfied with a Teacher one. In languages like Java, this is a no-go.
Of course, in our TypeScript snippet, if we add another property to the Teacher, we will be notified of it.
We might feel a false sense of security when using TypeScript, expecting the compiler to save us. But, in reality, we might be introducing similar bugs in our code if we're not careful.
How can we fix this?
We need a way to differentiate between the two types.
First, we create a symbol that we can use to identify our nominal types. It’s worth noting that it won’t exist after compilation.
Then we enrich our base type (e.g. string) with a symbol that we can use to identify our nominal types. Two types are considered equivalent if they have the same symbol, in addition to their contents.
Some examples, where we might need a distinction:
There you have it
The elephant in the room
Yes, we have to use as. We can’t assign the xyz string directly.
You might consider it cheating, but TypeScript has sharp knives in its feature set. It’s up to us to know when to use them.
That said, we can improve it. Let’s concentrate the as type-casting in one place.