Nominal typing on TypeScript
You can use "nominal typing" for a clearer and safer code, as well as to potentially reduce the need of data validation on multiple places.
TypeScript has type compatibility based on "structural typing" but we can get some kind of "nominal typing" with not much effort.
You can read more about TypeScript's type compatibility on their handbook.
Please read the references section on the bottom for information about "structural typing", "nominal typing" and the different approaches that can be used.
Example
Consider this person renaming function:
function renamePerson(id: string, newName: string): boolean {
The id
and newName
arguments can be any string
, they don't have to be valid id/name respectively (for whatever definition of valid we use on our app).
Because of that, the safest thing to do is to actually validate them as soon as we enter the function.
We would also have validation for the person name somewhere on the UI so we can give feedback to the user if there's a wrong name being used.
In order to make sure that the renamePerson
function actually receives valid data we can do something like this:
function renamePerson(id: PersonId, newName: PersonName): boolean {
Note that id
and newName
now have their own type.
How should we define those types?
We can't use something like type PersonName = string
because, due to TypeScript type compatibility being based on structural typing, you could send string
s for those parameters and that'll just work without complaints from the compiler.
There are several ways of doing this, I'll just pick the simplest one to explain the concept, once you're familiar with the idea you can choose from other variations.
We create a custom type with something specific to it to simulate "nominal typing". Here's an example:
// intersect string with something specific to prevent a structural typing match
type PersonName = string & { __brand: "valid person name" };
Let's use it:
function sayHello(name: PersonName) { /* ... */ }
sayHello("Alice");
// Argument of type 'string' is not assignable to parameter of type 'PersonName'.
// Type 'string' is not assignable to type '{ __brand: "valid person name"; }'.(2345)
Now, our PersonName
typed data is not "structurally compatible" with a string
. We have to use our own type.
// any variable of type PersonName must be created here
// that way we guarantee that validation happens
function getPersonName(personName: string): PersonName | undefined {
if (isValidPersonName(personName)) return;
return personName as PersonName;
}
function sayHello(name: PersonName) { /* ... */ }
const name = getPersonName("Alice");
if (name !== undefined) sayHello(name);
If we only create a PersonName
using this "constructor", and the validation is done properly, we can be certain that everywhere we see a PersonName
is going to be valid.
We could validate a string
as soon as we receive it from a user or from an API, and then create a PersonName
, from that point on we can be certain that we are dealing with valid data.
Playground
Here's a TypeScript playground with some example code: typescript playground
References
Here are some articles where You can read more about nominal typing:
- https://michalzalecki.com/nominal-typing-in-typescript/
- https://betterprogramming.pub/nominal-typescript-eee36e9432d2
- https://levelup.gitconnected.com/nominal-typing-in-typescript-c712e7116006
There's also TypeScript's handbook:
An example of this on TypeScript's playground:
Some relevant Stack Overflow answers:
P.S. about people names
I used people names for my example because it's easy to grasp for an example, but remember that defining what a valid name is is not straigforward. See the "Falsehoods Programmers Believe About Names" article:
Top comments (0)