Home

Union Types with Objects

3 min read
Two people hand in handPhoto by Aarón Blanco Tejedor

This post is part of the Typescript Learning Series.

When I was testing some ideas and API features for JavaScript dates, I built a project in Typescript. I wanted to build a more human-friendly API to handle dates.

This is what I was looking for:

get(1).dayAgo; // it gets yesterday

I also make it work for month and year:

get(1).monthAgo; // it gets a month ago from today
get(1).yearAgo; // it gets a year ago from today

These are great! But I wanted more: what if we want to get days, months, or years ago? It works too:

get(30).daysAgo;
get(6).monthsAgo;
get(10).yearsAgo;

And about the implementation? It is just a function that returns an JavaScript object:

const get = (n: number): DateAgo | DatesAgo => {
  if (n < 1) {
    throw new Error('Number should be greater or equal than 1');
  }

  const { day, month, year }: SeparatedDate = getSeparatedDate();

  const dayAgo: Date = new Date(year, month, day - n);
  const monthAgo: Date = new Date(year, month - n, day);
  const yearAgo: Date = new Date(year - n, month, day);

  const daysAgo: Date = new Date(year, month, day - n);
  const monthsAgo: Date = new Date(year, month - n, day);
  const yearsAgo: Date = new Date(year - n, month, day);

  if (n > 1) {
    return { daysAgo, monthsAgo, yearsAgo };
  }

  return { dayAgo, monthAgo, yearAgo };
};

And here we are! I want to tell you about Union Type with objects.

We have different return types depending on the n parameter. If the n is greater than 1, we return an object with "plural" kind of attributes. Otherwise, I just return the "singular" type of attributes.

Different return types. So I built the two types.

The DateAgo:

type DateAgo = {
  dayAgo: Date;
  monthAgo: Date;
  yearAgo: Date;
};

And the DatesAgo:

type DatesAgo = {
  daysAgo: Date;
  monthsAgo: Date;
  yearsAgo: Date;
};

And use them in the function definition:

const get = (n: number): DateAgo | DatesAgo =>

But this gets a type error.

When using:

get(2).daysAgo;

I got this error: Property 'daysAgo' does not exist on type 'DateAgo | DatesAgo'.

When using:

get(1).dayAgo;

I got this error: Property 'dayAgo' does not exist on type 'DateAgo | DatesAgo'.

The DateAgo doesn't declare the following types:

  • daysAgo
  • monthsAgo
  • yearsAgo

The same for the DatesAgo:

  • dayAgo
  • monthAgo
  • yearAgo

But it can have this properties in run-time. Because we can assign any kind of properties to an object. So a possible solution would be to add an undefined type to both DateAgo and DatesAgo.

type DateAgo = {
  dayAgo: Date;
  monthAgo: Date;
  yearAgo: Date;
  daysAgo: undefined;
  monthsAgo: undefined;
  yearsAgo: undefined;
};

type DatesAgo = {
  daysAgo: Date;
  monthsAgo: Date;
  yearsAgo: Date;
  dayAgo: undefined;
  monthAgo: undefined;
  yearAgo: undefined;
};

This will fix the issue in compile time. But with this, you'll always need to set an undefined value to the object. One to get around this is to add an optional to the undefined types. Like this:

yearAgo?: undefined

With that, you can set these undefined properties. A better solution is to use the never type:

"The never type represents the type of values that never occur."

type DateAgo = {
  dayAgo: Date;
  monthAgo: Date;
  yearAgo: Date;
  daysAgo?: never;
  monthsAgo?: never;
  yearsAgo?: never;
};

type DatesAgo = {
  daysAgo: Date;
  monthsAgo: Date;
  yearsAgo: Date;
  dayAgo?: never;
  monthAgo?: never;
  yearAgo?: never;
};

It works as expected and it also represents the data semantically as these attributes will not occur for both situations.

Resources

My Twitter and Github