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';
}
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;
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
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;
}
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
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 (2)
Nice article. Just want to point out that for this to work, you actually have to cast the return value to your coditional type, otherwise TS complains. Using your basic example, you'd get the following error:
But if you change the returns to look like this, the error goes away:
Another option is to use overloaded signatures, see here.
Thanks for adding this comment! Hope you enjoyed the post! :)