DEV Community

Nick
Nick

Posted on

Using Function Parameters with Conditional Types in Typescript

Lately, I find myself wanting to conditionally return one of two different types from a function, while still getting Typescript's static typing benefits. This is possible by constructing a conditional type in Typescript.

The concept itself is quite simple but becomes powerful when combined with querying data – which we will uncover towards the end of this post. But first, a contrived example...

Say you have two different interfaces a Cat interface and a Dog interface.

interface Cat {
 sound: 'meow';
}

interface Dog {
 sound: 'woof';
}
Enter fullscreen mode Exit fullscreen mode

And you would like to conditionally return one of the two from a function.

With conditional types, a ternary-like syntax can be used to construct a new type CatOrDog that can return either a Cat or Dog based on a generic.

type CatOrDog<T extends 'cat' | 'dog'> = T extends 'cat' ? Cat : Dog;
Enter fullscreen mode Exit fullscreen mode

This new type can then be used in a function to conditionally return a Cat or a Dog based on a function parameter to dictate which we should expect to receive from a function.

function getCatOrDog<T extends 'cat' | 'dog'>(option: T): CatOrDog<T>  {
 if (option === 'cat') {
  return {sound: 'meow'};
 }

 if (option === 'dog') {
  return {sound: 'woof'};
 }
}

getCatOrDog('cat'); // returns Cat
getCatOrDog('dog'); // returns Dog
Enter fullscreen mode Exit fullscreen mode

Now the above function will conditionally return a Cat or a Dog depending on what is passed into the option parameter. This leads to a great developer experience as you can take advantage of Typescript's static type checking here and modern IDEs can give you intelli-sense to flag any type errors when using this function during development.

The CatOrDog example is a contrived use of this pattern. The benefits become quite apparent when you combine this with fetching data from a database, which is where I find myself reaching for this pattern most often (usually with an ORM like TypeORM).

Say you have an application where you store user information in a table called user and contact information in a separate table called contact_info and there is a 1:1 relationship between the two tables, you may have interfaces set up similar to the following, which represent an entity structure in your database.

interface User {
 id: string;
 name: string;
}

interface ContactInfo {
 phone: string;
 address: string;
 userId: string;
}
Enter fullscreen mode Exit fullscreen mode

Then say when you query users you may want to conditionally hydrate the contact information for a user. You could make a conditional type that uses a function parameter, as we did before, to determine whether you should expect to receive an instance of User or User & ContactInfo. The function can use the same function parameter to add a join to a query based on the input value.

type UserOrUserWithContactInfo<T extends boolean> = T extends true ? User & ContactInfo : User;

async function fetchUser<T extends boolean>(id: string, withContactInfo: T): Promise<UserOrUserWithContactInfo<T>> {
 const query = `
  SELECT *
  FROM user
 `

 if (withContactInfo) {
  query += `
   LEFT JOIN contact_info on user.id = contact_info."userId"
  `
 }

 const query += `
  WHERE user.id = '${id}'
 `

 // assume this executes our query string
 return connection.query(query);
}

await fetchUsers(false); // returns User
await fetchUsers(true); // returns User & ContactInfo
Enter fullscreen mode Exit fullscreen mode

Now when using this function you can benefit from static type checking by conditionally adding a join to a query to fetch the contact information related to a user. This utilizes the new UserOrUserWithContactInfo conditional type which returns either a User or User & ContactInfo based on the boolean value that we pass to the withContactInfo function parameter.

You can read more about Typescript conditional types here.

Top comments (0)