Dynamically Allocate Function Types with Conditional Types in TypeScript

Rares Matei
InstructorRares Matei

Share this video with your friends

Send Tweet
Published 4 years ago
Updated 3 years ago

Conditional types take generics one step further and allow you to test for a specific condition, based on which the final type will be determined. We will look at how they work on a basic level and then leverage their power to allocate function types dynamically based on the type of their parameters, all without any overloads.

Instructor: [00:00] Generics have always been a good solution to making reusable types. You can pass in one type and return a completely new type from it. It's like a function, really. TypeScript's new conditional types take this to the next level.

[00:12] Instead of simply copying the value of whatever's passed in here to some location in the generated type, we can tell the compiler to intelligently analyze a ternary expression.

[00:23] If my generic parameter extends a string or is a string, then my container property will be of type string container. Otherwise, it's going be a number container. I've created in this, that's of an item down here. I'm passing in string as the generic parameter.

[00:37] Let's try this. If I try to access the container on it, we can see we get format, split, all of which are string container property. Now, let's try to pass in a number here, see what happens. As expected, we get number container properties now.

[00:51] I can also build an array filter type. All we have to say is that, if the generic parameter extends an array, then we just return that type. Otherwise, we shall return the never type, so the compiler can ignore it. Let's try this out.

[01:06] I'll create a new type called strings or numbers. I'll use the array filter type that I created. I'll pass in a type that can be either string or a number, or an array of strings, or an array of numbers.

[01:19] Now, if I hover over my new type, I can see that it's either the array of strings or the array of numbers. Anything that wasn't of type array has been filtered out. All of this works because of two mechanisms.

[01:32] Conditional types distribute over each element in the set of possible types that we pass into it. In my case, it's going to apply this ternary operation to each of these four types individually and replace each one of them with whatever this expression returns.

[01:50] In my case, it's going to return never for the first type. It's going to return never for the second type. It's going to return array of string and it's going to return array of numbers for the last type.

[02:01] Second, because by definition the never type can never happen, if TypeScript sees it in a union of types like we have here, it's just going to be ignored. What we end up in the end is a type that's either an array of string or array of numbers.

[02:18] Finally, conditional types can become a replacement for function overloads. I have here a type item service, which has a function getItem. This function is a bit versatile, because if the ID is a string, then it's going to return a book.

[02:33] That's because books are indexed by strings. If it's a number, it's going to return a TV. That's because TVs are indexed by numbers. Now, I can try to overload this function with a few definitions, and it works.

[02:46] Let's see if we can make use of conditional types and keep just the single definition. I'll just add in here. If key is a string, then return a book, otherwise, return a TV.

[03:00] I'll pretend this item service variable here exists, and it's instantiated. I'll attempt to get the book from it by passing in a string. Then, I'll do the same thing for the TV by passing in the number.

[03:12] If I hover over the book, I can see TypeScript correctly infer that it's a book. If I hover over the TV variable, TypeScript correctly inferred that it's a type TV. One problem with this approach is that I can pass in a Boolean in here. It's still going to work and TypeScript will think that this is still a TV.

[03:30] To stop that from happening, I can just lock in the possible types of the ID that can be sent in here to just strings and numbers. The moment I do that, TypeScript will start complaining here that I'm passing in something that's not really allowed.

Dean
Dean
~ 3 years ago

I tried to read on conditional types and nothing made sense... but your video really nailed it down in 3 minutes.. good job!!

Anup
Anup
~ 3 years ago

Excellent video!!! Can you please help me understand how one would implement getItem() and differentiate between whether id is a number or string?

Ian Jones
Ian Jones
~ 3 years ago

Anup, That's a good question. Let me see if I can find an answer.

Rares Matei
Rares Mateiinstructor
~ 3 years ago

Hi Anup, very good question!

The example I gave above is very useful to consumers of that API.

When implementing it unfortunately, there's a current a limitation of the TS compiler where it can't safely infer whether you're returning the correct value. More details here:

  • https://github.com/microsoft/TypeScript/issues/22735
  • https://github.com/microsoft/TypeScript/issues/24929

So you will have to use casting:

let itemService: IItemService = {
  getItem: <T extends string | number>(id: T) => {
    const books: Book[] = [
      { id: "1", tableOfContents: [] },
      { id: "2", tableOfContents: [] }
    ];
      const tvs: Tv[] = [
      { id: 1, diagonal: 40 },
      { id: 2, diagonal: 50 }
    ];
    if (typeof id === "string") {
      return <T extends string ? Book : Tv>books.find(book => book.id === id)!;
    } else {
      return <T extends string ? Book : Tv>tvs.find(tv => tv.id === id)!;
    }
  }
};