DEV Community

Nathaniel
Nathaniel

Posted on

1

Typescript function return type based on parameters

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' };
};
Enter fullscreen mode Exit fullscreen mode

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}`
  );
})();
Enter fullscreen mode Exit fullscreen mode

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' };
};
Enter fullscreen mode Exit fullscreen mode

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}`
  );
})();
Enter fullscreen mode Exit fullscreen mode

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>;
}
Enter fullscreen mode Exit fullscreen mode

Now the code is working as expected.

Image of Timescale

🚀 pgai Vectorizer: SQLAlchemy and LiteLLM Make Vector Search Simple

We built pgai Vectorizer to simplify embedding management for AI applications—without needing a separate database or complex infrastructure. Since launch, developers have created over 3,000 vectorizers on Timescale Cloud, with many more self-hosted.

Read more →

Top comments (1)

Collapse
 
arihantverma profile image
Arihant Verma • • Edited

Put the code together, it errors:

typescriptlang.org/play?#code/C4Tw...