Back

Typescript function return type based on parameters

Tue, Jan 12 20213 min read
Nathaniel
Typescript is all fun and games until you want some behaviour based on runtime values, recently I encountered a tricky problem: How do I type a function's return type based on the parameter value?
I know this sound like an anti-pattern but there are many real world use case for it, for example your function have an option field that will determine the type of value it returns:
type User = {
id: string;
firstName: string;
lastName: string;
profilePicture?: string | ProfilePicture;
};
type ProfilePicture = {
height: string;
width: string;
url: string;
};
const db = {
findUserById: async (userId: string): Promise<User> => ({
id: '1',
firstName: 'Bruce',
lastName: 'Wayne',
}),
};
const generateProfilePictureById = async (
userId: string
): Promise<ProfilePicture> => ({
height: '20px',
width: '20px',
url: `http://example.com/${userId}.png`,
});
const getUserProfile = async (
userId: string,
options?: { generateProfilePicture: boolean }
) => {
const user = await db.findUserById(userId);
if (options?.generateProfilePicture) {
return {
...user,
profilePicture: await generateProfilePictureById(userId),
};
}
return { ...user, profilePicture: 'picture not generated' };
};
Now if you want to use getUserProfile like:
(async () => {
const user = await getUserProfile('1', { generateProfilePicture: true });
console.log(
`${user.firstName} ${user.lastName} has profilePicture with height:
${user.profilePicture.height}`
);
})();
Typescript will complain that height does not exist on user.profilePicture
But you know that if generateProfilePicture option is set to true, user.profilePicture will not be the inferred type string | ProfilePicture
How do we solve this problem then? Typescript have the answer: Function overload
Basically, typescript will map multiple signatures of a function in an order of their appearance in code. It will use the first matching type signature for that function.
Knowing this, let's improve the typing of our function getUserProfile:
interface GetUserProfileType {
<T extends boolean>(
userId: string,
options?: { generateProfilePicture: T }
): Promise<
Omit<User, 'profilePicture'> & {
profilePicture: T extends true ? ProfilePicture : string;
}
>;
(
userId: string,
options?: { generateProfilePicture: boolean }
): Promise<User>;
}
const getUserProfile: GetUserProfileType = async (
userId: string,
options?: { generateProfilePicture: boolean }
) => {
const user = await db.findUserById(userId);
if (options?.generateProfilePicture) {
return {
...user,
profilePicture: await generateProfilePictureById(userId),
};
}
return { ...user, profilePicture: 'picture not generated' };
};
Now our user.profilePicture will be string when generateProfilePicture is false, and ProfilePicture when generateProfilePicture is true.
But wait, there's more
What if we omit the options entirely and use it like:
(async () => {
const user = await getUserProfile('1');
console.log(
`${user.firstName} ${user.lastName} has profilePicture with height:
${user.profilePicture.length}`
);
})();
Now for the above code typescript complains: Property 'length' does not exist on type 'ProfilePicture'. Apparently it did not match with any of the two function overloads. Well, guess three time is a charm, let's add the third function overload:
interface GetUserProfileType {
<T extends { generateProfilePicture: boolean } | undefined>(
userId: string,
options?: T
): Promise<
Omit<User, 'profilePicture'> & {
profilePicture: T extends undefined ? string : never;
}
>;
<T extends boolean>(
userId: string,
options?: { generateProfilePicture: T }
): Promise<
Omit<User, 'profilePicture'> & {
profilePicture: T extends true ? ProfilePicture : string;
}
>;
(
userId: string,
options?: { generateProfilePicture: boolean }
): Promise<User>;
}
Now the code is working as expected.

Comments(0)

Continue with
to comment