Tag Types in Typescript with taghiro

You can leave Feedback and follow me on Twitter.

Typescript has brought types in Javascript to the mainstream. With TS it's easier to write correct code and code that's easier to understand.

One step further are tag types. With them you can tag other types and you express refined types with richer constraints. I've written about them before in Scala here and here. For example NotZero is a tag type preventing bto be 0 in this example.

function divide(a:number, b:number & NotZero) {
....
}

I've written a tag type library in Typescript with a number of ready to use tag types to make your code richer and cleaner. It's called taghiro and can be found on Github. It's mission is to prevent bugs and make code more readable to developers.

Ready to use tag types

Some examples of the many ready to use tag types included in taghiro are

  • MinSize
  • NonEmpty
  • Sorted
  • Positive
  • UpperCase

Tag types can easily be used with Typescript Intersection types and type guards. Suppose we have a function that lists all product categories and takes a category as a parameter. In our system all categories have to be uppercase, e.g. ELECTRONICS.

To make sure the parameter is uppercase we declare it as string & UpperCase.

import { UpperCase } from 'taghiro';

function listProductCategory(
    category: string & UpperCase
) {
    ...
}

Developers easily can see the constraint of category: It must be uppercase. Compare this to a method signature where the constraint is in the documentation. Failure to adhere to the constraint is not detected at compile time but at runtime and depending on the error handling in listProductCategory the system might crash.

// category: string Needs to be uppercase
function listProductCategory(category: string) {
    ...
}

A method with tag types can be used with type guards. Type guards in Typescript ensure a variable has a certain type. After the check Typescript assumes the variable has the corresponding type without casting.

import { isUpperCase } from "taghiro";

if (isUpperCase(category)) {
  // category now has type string & UpperCase
  listProductCategory(category);
} else {
  console.log("This is not a category.");
}

isUpperCase is defined by taghiro

export type UpperCase = Tag<'uppercase'>;

export function isUpperCase<T extends number>(
    value: string
): value is string & LowerCase {
  return value.toUpperCase() == value;
}

Handling the case that category is not uppercase now lays in the responsibility of the caller, who is much better equipped to handle the case as there is more context. In most systems the transformation from string to string & UpperCase happens a the edges, while inside the system every method uses string & UpperCase and needs no more error handling code.

Now on to a second complex example. Suppose we want to write an API to send emails.

function sendEmails(
    to:Array<string>, 
    html:string
) {
....
}

Here several things could go wrong. First the to array could be empty. Second html could be empty, not contain any HTML or contain unsafe HTML. With tag types we can make sure the paramaters are save.

import { NonEmpty, isNotEmpty } from 'taghiro';

function sendEmails(
    to:Array<string & Email> & NonEmpty,
    html:string & NonEmpty) {
    ....
}

Now the caller needs to ensure that the parameters satisfy the tag types.

import { NonEmpty, isNotEmpty } from "taghiro";

// we assume here emails is already of type
// Array<string & Email>
if (isNotEmpty(emails) && isNotEmpty(html)) {
  sendEmails(emails, h
}

Another way would be to use custom types for the parameters. This has the drawback in Typescript that it doesn't prevent using the wrong type.

type ReceiverList = Array<string>;
type Html = string;

function sendEmails(
    to:   ReceiverList,
    html: Html
) {
    ....
}

Or we could use ReceiverList and Html classes, which is the usual way to use OO and then use the same type guards to check for NonEmpty.

interface ReceiverList {
    emails: Array<string>
}

The downside here is you need more classes and in a large project this leads to hundreds of smaller helper data classes. These need to be maintained and kept in your mind when you develop new parts of the system or change existing parts. One other drawback is that you can't put these values into methods that take string and Array<string> while you can do this with the tag types.

But the major downside is the OO argument of encapsulation. ReceiverList encapsulates Array<string>. This indirection is aimed to make it easier to understand systems while in reality this indirection adds another layer you need to be aware of. Array<string & Email> & NonEmpty can be understood by everyone new to the project without looking into more classes. Compare this to an ReceiverList constructor

const emails = new ReceiverList(theEmails);

were we still don't know without looking in the documentation the constraints on theEmails (non-empty and being emails).

Custom tags

With taghiro you can write your own Tags. By leveraging libraries for checking emails and HTML we can easily implement Email and SafeHtml to make the method even safer.

function sendEmails(
    to:Array<string @ Email> & NonEmpty,
    html:string & SafeHtml
) {
....
}

Custom tag types

Beside using the supplied tag types, it's adviced to use your own. Tag types can be used to define custom domain concepts. One example is id. Here is an example based on string Uuid ids.

import { Tag, isUuid } from "taghiro";

export type CustomerId = Tag<"customer-id">;

export function isCustomerId(value: string)
    : value is string & CustomerId {
  return isUuid(value);
}

This way it's impossible to put the wrong ids into a method.

function findCustomer(id: string & CustomerId) {
    ...
}

will only take a CustomerId. Compare this to

function findCustomer(id: string) {
    ...
}

where in complex projects it's easy to put the wrong id into the findCustomer method, producing a hard to find bug.

One can define a custom Tag type to define more than one id tag.

import { isUuid } from "taghiro";

export interface Id<T extends string> {
  readonly __id: T;
}

export type CustomerId = Id<"customer">;

export function isCustomerId(value: string)
    : value is string & CustomerId {
  return isUuid(value);
}

export type AccountId = Id<"account">;

export function isAccountId(value: string)
    : value is string & AccountId {
  return isUuid(value);
}

Tag types are an easy way to build on the already excellent type system of Typescript. taghiro supplies ready to use tag type so you will need to write less code. And it will help you write less bugs.

taghiro is Open Source and licensed under a MIT license.