Consistent Return Types in TypeScript

Aug 10, 2024

Imagine we have the following function:

type User = {
  id: string;
  permissions: string[];
};

async function getUserPermissions(userId: string) {
  const user = await db.getUserById(userId);
  if (!user) {
    return { found: false };
  }

  return user.permissions;
}

The above function has the following implicit return type:

Promise<string[] | { found: boolean }>

The issue is that the two return types have no properties in common, so if you try to do the following:

const userPermissions = await getUserPermissions('123');
if (!userPermissions.found) { // compiler error here
  // Handle user not found
} else if (userPermissions.includes('admin'))
  // Allow them access
}

the TypeScript compiler will yell at you with:

Property 'found' does not exist on type 'string[] | { found: boolean; }'

The Fix

The fix will depend on your business logic. In the above scenario, does your app need to distinguish between a user not being found and the user not having the necessary permissions? If not, you should do the following:

async function getUserPermissions(userId: string) {
  const user = await db.getUserById(userId);
  if (!user) {
    return [];
  }

  return user.permissions;
}

This way your function always returns string[]. But if your app does need to distinguish between a user not being found and the user not having the necessary permissions, returning null is probably best:

async function getUserPermissions(userId: string) {
  const user = await db.getUserById(userId);
  if (!user) {
    return null;
  }

  return user.permissions;
}

The return type of Promise<string[] | null> is much easier to work with:

const userPermissions = await getUserPermissions('123');
if (!userPermissions) {
  // Handle user not found
} else if (userPermissions.includes('admin'))
  // Allow them access
}

One Step Further

The easiest way to make sure you know exactly what type your function is returning is to explicitly add the return type:

async function getUserPermissions(userId: string): Promise<string[]> {
  const user = await db.getUserById(userId);
  if (!user) {
    return [];
  }

  return user.permissions;
}

This is a good habit to get into because if you do end up returning something other than string[] in the above function, the TypeScript compiler will yell at you.

If you're using eslint, I would recommend enabling the explicit-function-return-type rule, at least as a warning to start. This will ensure that your functions are returning exactly what you expect, with no surprises.