Union Types with Objects

·

0 min read

This post is part of the Typescript Learning Series. And it was originally published at TK's blog.

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