TypeScript Tip: Using Conditional Types To Refactor Overloads.

andersonjoseph

Anderson. J

Posted on April 4, 2022

TypeScript Tip: Using Conditional Types To Refactor Overloads.

Introduction

A few days ago I was refactoring code and I found something like this:

interface IRawUser {
  first_name: string
  email: string,
  id: number,
}

interface User {
  name: string,
  email: string
  print: () => void
}

declare function userFactory(rawUser: IRawUser): User;

function mapRawToUserObject(rawShow: IRawUser[]): User[];
function mapRawToUserObject(rawShow: IRawUser): User;
function mapRawToUserObject(rawShow: IRawUser | IRawUser[]): User | User[] {
  if (rawShow instanceof Array) {
    return rawShow.map((raw) => userFactory(raw));
  }

  return userFactory(rawShow);
}
Enter fullscreen mode Exit fullscreen mode

The function mapRawToUserObject uses overloads to express the following: If we call it with an array of IRawUser[] it will return an array of User[], if we call it with a single IRawUser it will return a single User.

Nothing too complex but it seemed like a good opportunity to refactorize the method definition.

Creating a conditional type.

We need to take a decision on the type of input that the method receives.

The possible inputs we need to look at are: IRawUser and IRawUser[]

type MapRawResult<T extends IRawUser | IRawUser[]> = any;
Enter fullscreen mode Exit fullscreen mode

Then we add the conditional logic:

type MapRawResult<T extends IRawUser | IRawUser[]> = T extends IRawUser ? User : User[]
Enter fullscreen mode Exit fullscreen mode

In plain words: If T is assignable to IRawUser return User otherwise return User[]

Applying the conditional type to our method.

Right now our method looks like this:

function mapRawToUserObject(rawShow: IRawUser[]): User[];
function mapRawToUserObject(rawShow: IRawUser): User;
function mapRawToUserObject(rawShow: IRawUser | IRawUser[]): User | User[] {
  // implementation
}
Enter fullscreen mode Exit fullscreen mode

We can now remove the overloads, add a generic parameter (T extends IRawUser | IRawUser[]), and replace the return type with the one we recently created.

type MapRawResult<T extends IRawUser | IRawUser[]> = T extends IRawUser ? User : User[]

function mapRawToUserObject<T extends IRawUser | IRawUser[]>(rawShow: T): MapRawResult<T> {
  // implementation
}
Enter fullscreen mode Exit fullscreen mode

At this point, typescript will complain about the type of value that we are returning.

The compiler can't infer the type of the value we are returning, a solution for this is to explicitly express the type we are expecting using the keyword as.

Our final method then looks like this:

function mapRawToUserObject<T extends IRawUser | IRawUser[]>(rawShow: T): MapRawResult<T> {
  if (rawShow instanceof Array) {
    return rawShow.map((raw) => userFactory(raw)) as MapRawResult<T>
  }

  return userFactory(rawShow) as MapRawResult<T>
}
Enter fullscreen mode Exit fullscreen mode

Conclusion

If we try with some values, we can see how our method behaves the same as before but without using overloads.

As a bonus we could use a type alias for IRawUser | IRawUser[] and make the code cleaner:

type MapRawArg = IRawUser | IRawUser[];

type MapRawResult<T extends MapRawArg> = T extends IRawUser ? User : User[];
function mapRawToUserObject<T extends MapRawArg>(rawShow: T): MapRawResult<T> {
  // ...
}
Enter fullscreen mode Exit fullscreen mode

Further Reading

πŸ’– πŸ’ͺ πŸ™… 🚩
andersonjoseph
Anderson. J

Posted on April 4, 2022

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related