DEV Community

TK
TK

Posted on • Originally published at leandrotk.github.io

Union Types with Objects

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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 }
};
Enter fullscreen mode Exit fullscreen mode

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
};
Enter fullscreen mode Exit fullscreen mode

And the DatesAgo:

type DatesAgo = {
  daysAgo: Date
  monthsAgo: Date
  yearsAgo: Date
};
Enter fullscreen mode Exit fullscreen mode

And use them in the function definition:

const get = (n: number): DateAgo | DatesAgo =>
Enter fullscreen mode Exit fullscreen mode

But this gets a type error.

When using:

get(2).daysAgo;
Enter fullscreen mode Exit fullscreen mode

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

When using:

get(1).dayAgo;
Enter fullscreen mode Exit fullscreen mode

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
};
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
};
Enter fullscreen mode Exit fullscreen mode

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

Resources

Top comments (0)