(You can also read the whole article on the TypeScript playground.)

Let’s say we have a database with articles and users. We want to write some function to get items from the database.

Let’s define what are articles an users first.


interface Article {
  title: string,
  content: string,
  date: number,
  hidden: boolean
  lang: "en" | "fr" | "es"
}

interface User {
  login: string,
  hashedPassword: string,
  salt: string,
  description: string,
}

Let’s create some users and articles.

const users: User[] = [
  { login: "Alice", hashedPassword: "a", salt: "ah", description: "Hi, I am Alice and this is my personal space." },
  { login: "Bob", hashedPassword: "b", salt: "bh", description: "Hello, I am Bob." },
  { login: "Carol", hashedPassword: "c", salt: "ch", description: "Hi, I am not Alice." },
];

const articles: Article[] = [
  { title: "Hello", content: "Goodbye.", date: 1, hidden: false, lang: "en" },
  { title: "Good morning", content: "Happy afternoon.", date: 2, hidden: true, lang: "en" },
  { title: "Hola", content: "¿Que tal?", date: 3, hidden: false, lang: "es" },
]

And let’s mix all of that in an array that will act as our database.

const actualDb: (User | Article)[] = [
  users[0], // 0: Alice
  articles[0], // 1: Hello
  users[1], // 2: Bob
  articles[2], // 3: Hola
  articles[1], // 4: Good morning
  users[2], // 5: Carol
]

Now we can write a function that gets an item from the database.

function getFromDb1(id: number) /* inferred return type is User | Article */ {
  return actualDb[id];
}

But what if there is no item in the database with the id we passed as a parameter? getFromDb1(42) would return undefined but the return type of the function doesn’t say undefined. There is two way to go about this.

  1. Change TypeScript configuration to enable noUncheckedIndexedAccess which says to the compiler that every access to an array index could return undefined, it would make the return type of getFromDb1 inferred as User | Article | undefined. Technically, it is true that every indexed access to an array can return undefined but it may be quite cumbersome if generalized to the whole codebase, for example, if you enable it you’ll see that the declaration of actualDb has an error at every line.
  2. Change the body of the function, see getFromDb2 below
function getFromDb2(id: number) /* inferred return type is User | Article | null */ {
  if (id >= actualDb.length) {
    return null; // we decide to return null instead of undefined here
  }
  return actualDb[id];
}

This is much better in my opinion since it actually forces us to check if the element is actually in the database.

let item1: User | Article = getFromDb2(2); // Good, the compiler give an error here "Type 'Article | User | null' is not assignable to type 'Article | User'. Type 'null' is not assignable to type 'Article | User'"
let item2: User | Article | null = getFromDb2(2); // This works, we explicitly says that item2 may be null
let item3 /* inferred type is User | Article | null */ = getFromDb2(2); // Obviously you don't need to explicitly write the type, TypeScript is able to perfectly infer it
let item4: User | Article = getFromDb1(2); // no error with the previous version, which is kind of dangerous because it can (and probably will at some point) return `undefined`

Get one item of a specific type

Now let’s say that you want to make a function to get a User by its id. You could do something like

function getUserById1(id: number) {
  return getFromDb2(id); // we use our best version of getFromDb
}

It would work as long as you’re passing an id that is from an actual User. But this function could also return a Article (the inferred return type of getUserById1 is User | Article | null), which is probably not what we want (it’s named “getUser” after all).

function getUserById2(id: number) /* inferred return type is User | null */ {
  const item = getFromDb2(id);
  if (item == null) {
    return null;
  }
  else if ("login" in item) {
    // Here TypeScript knows that item is a User because it has a login field, and between User and Article only User has a login field
    return item;
  }
  return null; // when item is not a User we return null, not much we can do here, the caller probably made a mistake, we could throw an exception but... (see after about exceptions)
}

Let’s do the same thing for Article.

function getArticleById2(id: number) /* inferred return type is Article | null */ {
  const item = getFromDb2(id);
  if (item == null) {
    return null;
  }
  else if ("title" in item) {
    return item;
  }
  return null;
}

Good. And now let’s say we want a function that can fetch any item from the database, check it it’s of the right type (User of Article) and actually return it only if it’s ok. Something like:

function getAnyItem(type: "article" | "user", id: number) /* inferred return type is User | Article | null */ {
  const item = getFromDb2(id);
  if (item == null) {
    return null;
  }
  else if (type == "article" && "title" in item) {
    return item;
  }
  else if (type == "user" && "login" in item) {
    return item;
  }
  else {
    return null;
  }
}

It looks good. Except that it’s not very practical to use.

function getIdFromUser() {
  // we use a random generator here but the idea is that the user is typing the id by hand so it could be anything
  return Math.round(Math.random() * 10); // a number between 0 and 9
}

let a /* inferred type is User | Article | null */ = getAnyItem("article", 1); // will return article Hello
let b /* inferred type is User | Article | null */ = getAnyItem("user", 0); // will return user Alice
let c /* inferred type is User | Article | null */ = getAnyItem("article", getIdFromUser()); // will return one of the article or null
let d /* inferred type is User | Article | null */ = getAnyItem("user", getIdFromUser()); // will return one of the user or null

We KNOW that c will be NOT be of type User. It can’t because if it’s something else the check type == "user" && "login" in item will be false and the function will return null.

In the same way we KNOW that d CANNOT BE of type Article.

Never the less the types of c and d are User | Article | null. It would be nice if because of the first parameter being “article” we were able to tell TypeScript that the return type was going to be an Article. Same thing when the first parameter is “user”.

Enter mapped types!

Mapped Types

interface DbObjectMapping {
  "article": Article,
  "user": User,
}

function getAnyItem2<T extends keyof DbObjectMapping>(type: T, id: number): DbObjectMapping[T] | null {
  // let's leave the body empty for now
  return null!; // just a hack to remove the warning from the TypeScript compiler, ignore it
}

A few explanations are probably necessary. First what does keyof DbObjectMapping means?

let e: keyof DbObjectMapping = "article"; // ok
e = "user"; // ok
e = "hello"; // error: Type '"hello"' is not assignable to type 'keyof DbObjectMapping'.

So keyof DbObjectMapping is basically "article" | "user", the possible values that the keys of DbObjectMapping could have. The keys are the named to the left of the “:” in DbObjectMapping.

Second, what does T extends keyof DbObjectMapping means? It means that we declare a type variable called T that must be of type keyof DbObjectMapping so either “article” or “user”. The type parameter must be of this type so it can only be “article” or “user”.

Third, what about DbObjectMapping[T]? Well now that we know that T is "article" | "user" it quite obvious. DbObjectMapping[T] will be User when T is "user" and Article when T is "article".

Let’s see how we can use that.

// reminder: [0: Alice, 1: Hello, 2: Bob, 3: Hola, 4: Good morning, 5: Carol]
let u1 /* inferred type is User | null */ = getAnyItem2<"user">("user", 0); // should return user Alice
let a1 /* inferred type is Article | null */ = getAnyItem2<"article">("article", 1); // should return article Hello
let a2 /* inferred type is Article | null */ = getAnyItem2<"article">("article", 2); // should return null since the id with id 2 is a User and we are asking for an Article

“But we are just writing “user” twice!? What’s the point?!” you may ask. And you are right. luckily for us, TypeScript is smart, you can just remove the first instance and it will deduce that since the parameter type is “user” (or “article”) then T must be “user” (or “article”).

let u3 /* inferred type is User | null */ = getAnyItem2("user", 0); // the type is inferred as getAnyItem2<"user">(type: "user", id: number): User | null
let a3 /* inferred type is Article | null */ = getAnyItem2("article", 1); // the type is inferred as getAnyItem2<"article">(type: "article", id: number): Article | null

Now let’s try to write the body of our function.

function getAnyItem3<T extends keyof DbObjectMapping>(type: T, id: number): DbObjectMapping[T] | null {
  const item = getFromDb2(id);
  if(item == null) {
    // No item with this id has been found
    return null;
  }
  else if (type == "article" && "title" in item) {
    // TypeScript knows that item is an Article here
    return item;
    /*
    We have an error here:
    Type 'Article' is not assignable to type 'DbObjectMapping[T]'.
      Type 'Article' is not assignable to type 'Article & User'.
        Type 'Article' is missing the following properties from type 'User': login, hashedPassword, salt, description
    */
  }
  else if (type == "user" && "login" in item) {
    // TypeScript knows that item is an User here
    return item;
    /*
    Error:
    Type 'User' is not assignable to type 'DbObjectMapping[T]'.
      Type 'User' is not assignable to type 'Article & User'.
        Type 'User' is missing the following properties from type 'Article': title, content, date, hidden, lang
    */
  }
  else {
    // the type parameter does not match the actual type of the item
    return null;
  }
}

We have errors here. Weird errors. And sadly I don’t know any clean way of fixing it. What happens is that TypeScript knows that DbObjectMapping[T] can be an Article or a User and understands it as the fusion of the two, as an objects with the field of BOTH. You can be sure of this because the following code does not give any error.

function getAnyItem4<T extends keyof DbObjectMapping>(type: T, id: number): DbObjectMapping[T] | null {
  return {
    // field from Article
    title: "Hello",
    content: "Goodbye.",
    date: 1,
    hidden: false,
    lang: "en",
    // fields from User
    login: "Alice",
    hashedPassword: "a",
    salt: "ah",
    description: "Hi, I am Alice and this is my personal space.", // if we remove this line there will be an error: Property 'description' is missing in type
  }
}

You can check this Stack Overflow thread for an other explanation.

We can “fix” the function this way:

function getAnyItem5<T extends keyof DbObjectMapping>(type: T, id: number): DbObjectMapping[T] | null {
  const item = getFromDb2(id);
  if(item == null) {
    return null;
  }
  else if (type == "article" && "title" in item) {
    return item as DbObjectMapping[T]; // we basically say to TypeScript to ignore the error because we know what we are doing
  }
  else if (type == "user" && "login" in item) {
    return item as DbObjectMapping[T]; // we basically say to TypeScript to ignore the error because we know what we are doing
  }
  else {
    return null;
  }
}

And since we are doing the same thing when type == "article" && "title" in item and when type == "user" && "login" in item we can simplify the function to:

function getAnyItem6<T extends keyof DbObjectMapping>(type: T, id: number): DbObjectMapping[T] | null {
  const item = getFromDb2(id);
  if(item == null) {
    return null;
  }
  // we merge the two conditions here
  else if ((type == "article" && "title" in item) || (type == "user" && "login" in item)) {
    return item as DbObjectMapping[T];
  }
  else {
    return null;
  }
}

Not really satisfying if you ask me but I don’t know any other way to do it.

Why Not Use Exceptions?

When we wrote getUserById2 (duplicated below as getUserById22) I said that instead of returning null we could theoratically throw an exception but… and I didn’t explain why. Well it’s simple. There is no way in TypeScript to specify which exception a function might throw, which means that you can’t statically enforce catching them and I don’t like that.

function getUserById22(id: number) /* inferred return type is User | null */ {
  const item = getFromDb2(id);
  if (item == null) {
    return null;
  }
  else if ("login" in item) {
    // Here TypeScript knows that item is a User because it has a login field, and between User and Article only User has a login field
    return item;
  }
  return null; // when item is not a User we return null, not much we can do here, the caller probably made a mistake, we could throw an exception but... (see after about exceptions)
}

So if instead we threw an exception we would have something like;

class ItemNotFound extends Error {};
class ItemOfWrongType extends Error {};

function getUserById23(id: number) /* inferred return type is User */ {
  const item = getFromDb2(id);
  if (item == null) {
    throw new ItemNotFound();
  }
  else if ("login" in item) {
    // Here TypeScript knows that item is a User because it has a login field, and between User and Article only User has a login field
    return item;
  }
  throw new ItemOfWrongType();
}

Notice how the return type is now just User instead of User | null. For the caller it looks like the call can’t fail. We can write:

{
let u = getUserById23(1); // it will throw an ItemOfWrongType exception since the item of id 1 is an Article
console.log(u.login); // no error from the compiler, which make sense if you think about it, we will never reach this line since the previous line will throw an exception
let u2 = getUserById23(42); // it will throw an ItemNotFound exception since there is no item with and id of 42
console.log(u2.login);
}

Those call will fail and the compiler doesn’t warn us. With the previous version it would have said something:

{
let u = getUserById22(1);
console.log(u.login); // with getUserById22 the compiler is able to warn us than u may be null
let u2 = getUserById22(42);
console.log(u2.login); // same for u2
}

TypeScript could support some syntax to declare which exception a function can throw and force the caller to catch it. Like so (this syntax is NOT valid):

function getUserById24(id: number) /* throws ItemNotFound, ItemOfWrongType */ {
  const item = getFromDb2(id);
  if (item == null) {
    throw new ItemNotFound();
  }
  else if ("login" in item) {
    // Here TypeScript knows that item is a User because it has a login field, and between User and Article only User has a login field
    return item;
  }
  throw new ItemOfWrongType();
}

{
let u = getUserById24(1); // and here we could have a compiler error saying something like "getUserById24 can throw ItemNotFound or ItemOfWrongType but they aren't catched"
console.log(u.login);
}

But TypeScript doesn’t support that. And it’s not in the cards.

The good news is that you can still use Either types or declare the function like this function getUserById24(id: number) : ItemNotFound | ItemOfWrongType | User and check the return type with instanceof Error.