[SOLVED] How to accomplish stongly-typed ids in TypeScript?

Issue

A strongly-typed id would make it more difficult to pass the wrong id as a parameter to a function. E.g. getThingById(notAThing.id) is an easy mistake to make if both Thing and NotAThing have the same type of id.

Here’s a first crack it but in the end Id<TModel, TId> are both number, so it does not accomplish the goal. How can this be altered so that the final two function calls fail?

type Thing = { id: Id<Thing> };
type NotAThing = { id: Id<NotAThing> };

type Id<TModel, TId = number> = TId;

const thing: Thing = {
  id: 123,
};

const notAThing: NotAThing = {
  id: 456,
};

function getThing(id: Id<File>) {
  return "here's the thing";
}

function getNotAThing(id: Id<NotAThing>) {
    return "here's the not a thing";
}


// This works.
getThing(thing.id);
getNotAThing(notAThing.id);


// How to make it not work (fail compilation) if you pass the "wrong type of id"?
getThing(notAThing.id);
getNotAThing(thing.id);

Ready to test in the Playground

Solution

The concept is called "opaque types". It is a type similar to an existing one (like a string or a number) but not interchangeable with it.

There is a great implementation of this described by Drew Colthorp in the article Flavoring: Flexible Nominal Typing for TypeScript which uses TypeScript branding but allows for implicit conversions. It looks like this:

interface Flavoring<FlavorT> {
  _type?: FlavorT;
}
export type Flavor<T, FlavorT> = T & Flavoring<FlavorT>;

Which you can then use as (example from the article

type PersonId = Flavor<number, "Person">
type BlogPostId = Flavor<number, "BlogPost">

And now you can assign numbers to both of these but not cross assign values defined as PersonId and BlogPostId

let p: PersonId = 1;
let b: BlogPostId= 2;

p = b; //error
b = p; //error

Playground Link


For your code, with minimal changes, you can define your Id type as

type Id<T> = Flavor<number, T>;

And then use it by passing a string:

type Thing = { id: Id<"Thing"> };
type NotAThing = { id: Id<"NotAThing"> };

Which now makes your example behave as you expect: Playground Link

Answered By – VLAZ

Answer Checked By – Katrina (BugsFixing Volunteer)

Leave a Reply

Your email address will not be published. Required fields are marked *