DEV Community

Nick
Nick

Posted on • Edited 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...

A Basic 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

The above function will conditionally return a Cat or a Dog type 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 some modern IDEs can give you intelli-sense to flag any type errors when using this function during development.

Now that we have a basic example in place, let's move on to a more useful situation...

A Practical Use Case

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)

Image of Docusign

🛠️ Bring your solution into Docusign. Reach over 1.6M customers.

Docusign is now extensible. Overcome challenges with disconnected products and inaccessible data by bringing your solutions into Docusign and publishing to 1.6M customers in the App Center.

Learn more