Adding type safety to object IDs in TypeScript

A code snippet demonstrating type safety of IDs in TypeScript

I was creating a type for an options object of a function, one of the options properties would accept an ID of the item (string based UUID) or a special value, i.e. 'currentNode'.

Initially I would come up with something like this:

interface InsertOptions {
  // Insert into the item by id or the active item
  target: string | 'currentNode'
}

function insert(options: InsertOptions): void {
  //
}

insert({ target: 'activeNode' }); // Passing a clearly unintended option value will not result in an error
Code language: TypeScript (typescript)

Try It

There are 2 problems with this approach:

  • No autotype hints for currentNode
  • No type checking for the special option since curentNode is a also just a string.

type NodeId = string would hardly help since it’s just an alias for string and is the same from type checking perspective.

One way to solve this would be to wrap the special option in an object:

interface InsertOptions {
  target: string | { target: 'activeNode'}
}

function insert(options: InsertOptions): void {
  //
}

insert({ target: 'KQER3XVBdD3u' }); // Valid
insert({ target: { target: 'activeNode' } }); // Valid
insert({ target: { target: 'currentNode' } }); // ErrorCode language: TypeScript (typescript)

Try It

This is going to work, both type checking and autoexpand suggestion will work, but it does not look nice at all…

Fortunately TypeScript has template literal types since version 4.1. It’s possible to use them to create a type that only an ID would have, like:

type NodeId = `node_${string}`
Code language: JavaScript (javascript)

So now we can do:

interface InsertOptions {
  target: NodeId | 'activeNode'
}

function insert(options: InsertOptions): void {
  //
}

insert({ target: 'activeNode' }); // Valid
insert({ target: 'node_KQER3XVBdD3u' }); // Valid
insert({ target: 'currentNode' }); // ErrorCode language: TypeScript (typescript)

Try It

Now the target type is not string anymore and TS can see the difference between the ID and the special options, meaning that the special options are type checked and will show in autotype suggestions.

This does not only solve the 2 problems mentioned, but also introduces type safety of IDs in TypeScript. Each type of item can have a different type of ID assigned, I.e.

type UserId = `user_${string}`;
type GroupId = `group_${string}`;

function updateUser(id: UserId): void {
  //
}

// Imagine somewhere in the code you have an id variable that is a GroupId
declare const id: GroupId;

updateUser(id); // Error: Argument of type '`group_${string}`' is not assignable to parameter of type '`user_${string}`'Code language: JavaScript (javascript)

Try It

I have been using it for a year now and implementing type safe IDs has already prevented me a few times already from mixing entity types.

The benefit of this is not just at compile time, but also at runtime. You can use this to detect the type of object by checking its ID prefix and it’s also very convenient that whenever the object pops up in the logs, you immediately know what it is just by looking at the ID.