# Chapter 6: Conditional types In chapter 4, we learned how to move through the type space with union and intersection types, and how to create specific sets of values for our data structures. In chapter 5 we generalized type behaviour and bound types at a later stage, making our functions and classes flexible, yet specific to defined sets. But what if the behaviour of our types is ambiguous. What if there's more than one answer to a generic type? Or the type output simply "depends"? You know, how we see it in JavaScript all the time. With conditional types, we get the last tool in our journey to make most sense out of JavaScript code. Conditional types allow us to validate an input type's set, and decide on an output type based on this condition. If/else statements, but on a type level. This sounds complicated. To be true, some conditional types can mind-blowingly hard to understand, and their potential is sometimes hard to grasp. But this is what we we want to clear up! Let's make type level arithmetic approachable, and usable! To illustrate the features of conditional types, we are going to look at an e-Commerce application that sells physical audio mediums to collectors. CDs, LPs, and even tapes! ## Lesson 36: If this, then that Consider the following data structure we set up for our e-Commerce shop. We have customers, products, and orders. Customers have an ID, a first name, and a last name. ```typescript type Customer = { customerId: number, firstName: string, lastName: string } const customer = { id: 1, firstName: 'Stefan', lastName: 'Baumgartner' } // = type Customer ``` The product has a product ID, a title, and a price. ```typescript type Product = { productId: number, title: string, price: number } const product = { id: 22, title: 'Form Design Pattenrs', price: 29 } ``` The order has an ID as well, a customer (of type Customer), a list of products we buy with this order, and a date. ```typescript type Order = { orderId: number, customer: Customer, products: Product[], date: Date } ``` This is obviously very simplified, but a good start for our little app. We are implementing an administration interface for an e-Commerce application. Wewant to provide a `fetchOrder` function, which works as follows: 1. If we pass a customer, we get a list of orders from this customer. 2. If we pass a product, we get a list of orders that include this product. 3. If we pass an order ID, we just get this particular order. Our first idea to implement this would be function overloads. ```typescript function fetchOrder(customer: Customer): Order[] function fetchOrder(product: Product): Order[] function fetchOrder(orderId: number): Order function fetchOrder(param: any): any { // implementation to follow 😎 } ``` This works well for simple cases where we absolutely know which parameters we expect: ```typescript fetchOrder(customer) // it's Order[] fetchOrder(2) // it's Order ``` But it get's hairy when our input is ambiguous. When we pass an argument that can be either `Customer` or `number`, the output is a bit boring: ```typescript declare const ambiguous: Customer | number fetchOrder(ambiguous) // it's any 😕 ``` Of course, we could patch the types of the implementation function to be a bit more clearer: ```typescript function fetchOrder(customer: Customer): Order[] function fetchOrder(product: Product): Order[] function fetchOrder(orderId: number): Order function fetchOrder( param: Customer | Product | number ): Order[] | Order { // implementation to follow 😎 } ``` But when we want to be explicit about all possible outcomes, this gets very verbose, very soon: ```typescript function fetchOrder(customer: Customer): Order[] function fetchOrder(product: Product): Order[] function fetchOrder(orderId: number): Order function fetchOrder( param: Customer | Product ): Order[] function fetchOrder( param: Customer | number ): Order[] | Order function fetchOrder( param: Product | number ): Order[] | Order function fetchOrder( param: Customer | Product | number ): Order[] | Order { // I hope I didn't forget anything 😬 } ``` Seven overloads for 3 possible input types, and two possible output types. Now add another one, it's exhausting! ### Enter conditional types This has to be easier. We can map each input type to an output type - If the input type is `Customer`, the return type is `Order[]` - If the input type is `Product`, the return type is `Order[]` - If the input type is `number`, the return type is `Order` If the input type is a combination of available input types, the return types are a combination of the respective output types. We can model this behavour with conditional types. The syntax for conditional types is based on generics and as follows: ```typescript type Conditional<T> = T extends U ? A : B ``` Where T is the generic type parameter. U, A, and B are other types. We can read this statement like ternary operations in JavaScript: ```javascript const x = (t > 0.5) ? true : false ``` The above statement reads that if `t` is bigger than 0.5, then `x` is `true`, otherwise it's `false`. We can read the conditional type statement the same. If type `T` extends type `U`, the assigned type is `A`, otherwise it's `B`. Let's see how this works with the `fetchOrder` function. First, we create a type for all possible inputs. ```typescript type FetchParams = number | Customer | Product; ``` Then, we create a generic type `FetchReturn<T>` with a generic constraint to `FetchParams`. ```typescript type FetchReturn<Param extends FetchParams> = Param extends Customer ? Order[] : Param extends Product ? Order[] : Order ``` The type constraint `<Param extends FetchParams>` already reduces the available input types to three possible types. So this condition is already checked. The conditional then reads: 1. I f the `Param` type extends `Customer`, we expect an `Order[]` array 2. Else, if `Param` extends `Product`, we also expect an `Order[]` array. 3. Otherwise, when only `number` is left, we expect a single `Order`. In TypeScript jargon, we say the conditional type *resolves* to `Order[]`. Let's adapt our function to work with the new conditional type: ```typescript function fetchOrder<Param extends FetchParams>( param: Param ): FetchReturn<Param> { // Well, the implementation 😎 } ``` This is all we need to get the required return types for every combination of input types. ```typescript fetchOrder(customer) // Order[] 👍 fetchOrder(product) // Order[] 👍 fetchOrder(2) // Order 👍 fetchOrder(ambiguous) // Order | Order[] declare x: any // 💥 any is not part of `FetchParams` fetchOrder(x) ``` Conditional types also work well with the idea of having a type layer around regular JavaScript. They work only in the type layer and can be easily erased, while still being able to describe all possible outcomes of a function. ## Lesson 37: Combining function overloads and conditional types In the previous lesson we stated that conditional types are capable of describing everything that function overloads can do, and are much more correct. While this is *technically* true, there are scenarios where a healthy mix of function overloads and conditional types create much better readability and clearer outcomes. One scenario is dealing with optional arguments. The `fetchOrder` function is synchronous. And as we know, fetching something from a database or a back-end is most of the times happening asynchronously. Let's refactor `fetchOrder` so it allows for asynchronous data retrieval. The function should combine two different asynchronous patterns: 1. If we pass a single argument (either number, `Customer` or `Product`), we get a Promise in return with the respective outcome (`Order` or `Order[]`) 2. We are able to pass a callback as a second argument. This callback gets the result (`Order` or `Order[]`) as parameter, the function `fetchOrder` returns void. This is a classical pattern that we can see in many Node.js libraries. Either we pass a callback, or we return a promise. The interesting piece of this example is that the second argument is entirely optional. This means that the function shape can be very different. Let's look at each function head separately. ```typescript // A callback helper type type Callback<Res> = (result: Res) => void // Version one. Similar to the version from // the previous lesson, but wrapped in a promise function fetchOrder<Par extends FetchParams>( inp: Par ): Promise<FetchReturn<Par>> // Version two. We pass a callback function that // gets the result, and return void. function fetchOrder<Par extends FetchParams>( inp: Par, fun: Callback<FetchReturn<Par>> ): void ``` With a function shape that is so different, it's not sufficient enough to do a conditional type for a simple union. ### Tuple types for function heads A possible solution would be to do a conditional type for a union of the entire set of function heads. In JavaScript, we have the possibility to condense all function arguments into a tuple with rest parameters. ```typescript function doSomething(...rest) { return rest[0] + rest[1] } // returns "JavaScript" doSomething('Java', 'Script') ``` This rest parameter can be typed as a tuple. Let's type the callback version's arguments as tuple: ```typescript function fetchOrder<Par extends FetchParams>( ...args: [Par, Callback<FetchReturn<Par>>] ): void ``` And, let's also type the promise version's arguments as a tuple. ```typescript function fetchOrder<Par extends FetchParams>( ...args: [Par] ): Promise<FetchReturn<Par>> ``` We sum up the entire argument list of each function head into separate tuple types. This means, that we can create a conditional type that selects the right output type. ```typescript // a small helper type to make it easier to // read type FetchCb<T extends FetchParams> = Callback<FetchReturn<T>> type AsyncResult< FHead, Par extends FetchParams > = FHead extends [Par, FetchCb<Par>>] ? void : FHead extends [Par] ? Promise<FetchReturn<T>> : never; ``` The conditional type reads as follows: 1. If the function head `FHead` is a subtype of tuple `FetchParams` and `FetchCb`, then return void. 2. Otherwise, if the function head is a subtype of the tuple `FetchParams`, return a Promise. 3. Otherwise, never return We can use this newly created conditional type and bind it to our function. ```typescript function fetchOrder< Par extends FetchParam, FHead >(...args: FHead) : AsyncResult<FHead, Par> ``` And this pretty much does the trick. But it also comes at a high price: 1. Readability. Conditional types are already hard to read. In this case, we have two nested conditional types. The old `FetchReturn`, that reliably returns the respective return type. And the new `AsyncResult`, that tells us if we get void or a Promise back. 2. Correctness. Somewhere along the way we might lose binding information for our generic type parameters. This means that we don't get the *actual* return type, but a union of all possible return types. Making sure that we don't lose anything requires us to bind a lot of parameters, thus crowding our generic signatures and generic constraints. In cases like this, it might be a better idea to still rely on function overloads. ### Function overloads are fine Rewind to our initial function description. We described two possible versions and their outcomes. This mirrors exactly the function overloads we would've done without conditional types. So let's see how we can implement the whole set of possible functions: ```typescript // Version one function fetchOrder<Par extends FetchParams>( inp: Par ): Promise<FetchReturn<Par>> // Version two function fetchOrder<Par extends FetchParams>( inp: Par, fun: Callback<FetchReturn<Par>> ): void // The implementation! function fetchOrder<Par extends FetchParams>( inp: Par, fun?: Callback<FetchReturn<Par>> ): Promise<FetchReturn<Par>> | void { // fetch the result const res = fetch(`/backend?inp=${JSON.stringify(inp)}`) .then(res => res.json()) // if there's a callback, call it if(fun) { res.then(result => { fun(result) }) } else { // otherwise return the result promise return res } } ``` If we look closely, we see that we don't leave conditional types completely. The way we treat the `FetchReturn` type is still a conditional type, based on the `FetchParams` union type. The variety of inputs and outputs was nicely condensed into a single type. However, the complexity of different function heads was better suited as function overloads. The input and output behaviour is clear and easy to understand, the function shape is different enough to qualify for being defined explicitly. As a rule of thumb for your functions: 1. If your input arguments rely on union types, and you need to select a respective return type, then a conditional type is the way to go. 2. Is the function shape different (e.g. optional arguments), and the relationship between input arguments and output types easy to follow, a function overload will do the trick. ## Lesson 38: Distributive conditionals Before we continue into the realms of conditional types with more examples, let's hang out with the one conditional type we just wrote. ```typescript type FetchParams = number | Customer | Product type FetchReturn<Param extends FetchParams> = Param extends Customer ? Order[] : Param extends Product ? Order[] : Order ``` Remember the metaphor from the previous chapter. Generics work like functions, have parameters, and return output. With that in mind, we can see how this conditional type works when we put in one type as argument. Let's bind `Param` to `Customer`: ```typescript type FetchByCustomer = FetchReturn<Customer> ``` Subtitute with the conditional's definition: ```typescript type FetchByCustomer = Customer extends Customer ? Order[] : Customer extends Product ? Order[] : Order ``` Run through the conditions and get to a result. ```typescript type FetchByCustomer = Order[] ``` We can run through the same process with all other compatible types. ### Distribution over unions It gets a little different once we pass union types. In most cases, conditional types are distributed over unions during instatiation. Let's see how this works in practice. First, we instatiate `FetchParam` with a union of `Product` and `number`. ```typescript type FetchByProductOrId = FetchReturn<Product | number> ``` `FetchReturn` is a distributive conditional type. This means that each constituent of the generic type parameter is instatiated with the same conditional type. Or in short: A conditional type of a union type is like a union of conditional types. ```typescript type FetchByProductOrId = ( Product extends Customer ? Order[] : Product extends Product ? Order[] : Order ) | ( number extends Customer ? Order[] : number extends Product ? Order[] : Order ) ``` Again, we run through the conditions to get a result. ```typescript type FetchByProductOrId = Order[] | Order ``` And this is our expected result! Knowing that TypeScript's conditional types work through distribution is incredibly important for a variety of reasons. 1. We can track each input type to exactly one output type. No matter in which combination they occur. 1. This means that in a scenario like ours, where we want to have different return types for different input types, we can be 100% sure that we don't forget a combination. The possible combinations of return types is exactly the possible combinations of input types. Even though the possible combinations are the same, return type unions remove dublicates and impossible results. Which means that if we do a distribution over all possible input types, we get two output types in the result: ```typescript type FetchByProductOrId = FetchReturn<Product | Customer | number> // equals to type FetchByProductOrId = ( Product extends Customer ? Order[] : Product extends Product ? Order[] : Order ) | ( Customer extends Customer ? Order[] : Customer extends Product ? Order[] : Order ) | ( number extends Customer ? Order[] : number extends Product ? Order[] : Order ) // equals to type FetchByProductOrId = Order[] | Order[] | Order // removed redundancies type FetchByProductOrId = Order[] | Order ``` This is a feature that is becoming important later in this chapter. ### Naked types An important precondition to distributive conditional types is that the generic type parameter is a *naked* type. *Naked* type is type system jargon and means that the type parameter is present *as is*, without being part of any other construct. Being naked is the most common case for generic type parameters, the non naked version can lead to interesting side effects. Let's wrap the type parameter in a tuple type. ```typescript type FetchReturn<Param extends FetchParams> = [Param] extends [Customer] ? Order[] : [Param] extends [Product] ? Order[] : Order ``` For single type bindings, the conditional type works as before: ```typescript type FetchByCustomer = FetchReturn<Customer> type FetchByCustomer = // this condition is still true! [Customer] extends [Customer] ? Order[] : [Customer] extends [Product] ? Order[] : Order type FetchByCustomer = Order[] ``` The tuple `[Param]` when instatiated with `Customer` is still a sub-type of the tuple `[Customer]`, so this condition still resolves to `Order[]`. When we instatiate `Param` with a union type, and this doesn't get distributed, we get the following result: ```typescript type FetchByCustomerOrId = FetchReturn<Customer | number > type FetchByProductOrId = // this is false! [Customer | number] extends [Customer] ? Order[] : // this is obviously also false [Customer | number] extends [Product] ? Order[] : // so we resolve to this Order type FetchByProductOrId = Order // 😱 ``` `[Customer | number]`, being a wider type is a super type of `[Customer]`, and therefor not extending `[Customer]`. This means that no condition applies, and our conditional type falls through to the last option, `Order`. And this is a false result. To make this conditional type a lot more safe and correct, we can add another condition to it, where we check for the subtype of *number*. The last conditional branch resolves to *never*. ```typescript type FetchReturn<Param extends FetchParams> = [Param] extends [Customer] ? Order[] : [Param] extends [Product] ? Order[] : [Param] extends [number] ? Order : never ``` This makes sure that we definitely get the correct return value if we work with a single type. Union types always resolve to never, which can be a nice way of making sure that we *first* narrow down to a single constituent of the union. ## Lesson 39: Filtering with never The distributive property of conditional types allows for some interesting usecases when combined with *never*. It's possible to create useful filter types as building blocks for advanced, self-maintaining types in our applications. Remember the ultimate goal: We want to model data and behaviour, but never maintain our types beyond the model. ### The model Our e-Commerce application gets another feature. We want to create CDs and LPs with a `createMedium` function. This is how our model looks like. The type `Medium` contains our base properties: ```typescript type Medium = { id: number, title: string, artist: string, } ``` `TrackInfo` stores the number of tracks and the total duration. ```typescript type TrackInfo = { duration: number, tracks: number } ``` A `CD` is a combination of `Medium` and `TrackInfo`. We also add a kind to create discriminated unions. ```typescript type CD = Medium & TrackInfo & { kind: 'cd' } ``` An `LP` is also derived from the base `Medium` class. It contains two sides which store `TrackInfo` each: ```typescript type LP = Medium & { sides: { a: TrackInfo, b: TrackInfo }, kind: 'lp' } ``` We combine all possible media in an `AllMedia` union type. We also define a union of media keys. ```typescript type AllMedia = CD | LP type MediaKinds = AllMedia['kind'] // 'lp' | 'cd' ``` These are our types. The function `createMedium` should work as follows: 1. The first argument is the type we want to create, either an LP or a CD. 2. The second argument is all the missing info we need to successfully create this medium. We don't need the properties type, which we defined in our first argument, nor the ID, as this will be generated by the function. 3. The function returns the newly created medium. On to the implementation ### Select branches of unions The bare minimum of the function head defines to types for the arguments and `AllMedia` for the return type. Alternatively, this can be `Medium` to point to the base type. ```typescript declare function createMedium(kind, info): AllMedia ``` The first type we can define is which kind we want to select. It has to be of type `MediaKinds`. ```typescript declare function createMedium( kind: MediaKinds, info ): AllMedia ``` We use a generic to bind the actual value type if we use a literal. ```typescript declare function createMedium< Kin extends MediaKinds >( kind: Kin, info ): AllMedia ``` Now that we know which kind of medium we want to create exactly, we can focus on the expected output. AllMedia is definitely too wide, but how can we select a certain branch in our union? Remember that conditional types are distributed over union types, meaning that a conditional of unions is like a union of conditionals. We can use this behaviour to create a conditional type that checks if each constituent of a union is a subtype of the kind we are filtering for. If so, we return the constituent. If not, we return `never`. ```typescript type SelectBranch<Brnch, Kin> = Brnch extends { kind: Kin } ? Brnch : never ``` Note the naked type `Brnch`! Let's see what happens if we run `AllMedia` through it, and select the branch for `cd`. ```typescript // We create a type where we want to select the // cd branch of the AllMedia Union type SelectCD = SelectBranch<AllMedia, 'cd'> // this equals to type SelectCD = SelectBranch<CD | LP, 'cd'> // A conditional of unions is like a union of // conditionals type SelectCD = SelectBranch<CD, 'cd'> | SelectBranch<LP, 'cd'> // substitute for the implementation type SelectCD = (CD extends { kind: 'cd' } ? CD : never) | (LP extends { kind: 'cd' } ? LP : never) // evaluate! type SelectCD = // this is true! Awesome! Let's return CD (CD extends { kind: 'cd' } ? CD : never) | // this is false, let's return never (LP extends { kind: 'cd' } ? LP : never) // equals to type SelectCD = CD | never ``` We end with a union of `CD | never`. Again, for each constituent of a union, we get a proper type. However, `never` is the impossible type. As it says in the name, this can never happen! That's why TypeScript is removing everything that resolves to *never* from the union, if there are other constitunents available. So `SelectBranch<AllMedia, 'cd'>` resolves to `CD` eventually. Let's update our return type. ```typescript declare function createMedium< Kin extends MediaKinds >( kind: Kin, info ): SelectBranch<AllMedia, Kin> ``` By binding `Kin` to the value type, we get the correct branch of our union type. Handy! Also, if you use the pattern of adding a `kind` property to create discriminated unions a lot, the `SelectBranch` type becomes a reusable helper type in your arsenal of types. ### Extract A much more generic type is the built-in helper type `Extract<A, B>`. `Extract<A, B>` is defined as. ```typescript type Extract<A, B> = A extends B ? A : never ``` The documentation says that it extracts from A those types that are assignable to B. This can be a set of keys, or in our case, objects. ```typescript // resolves to LP type SelectLP = Extract<AllMedia, { kind: 'lp' }> ``` The moment we add another medium to the union type `AllMedia`, all our types are getting updated automatically. We have new kinds we can pass to `createMedium`, but also know that we're getting another Medium back. No maintenance from our side. We just add something to the model. ## Lesson 40: Composing helper types A quick look back to the previous lesson. This is how far we got: ```typescript declare function createMedium< Kin extends MediaKinds >( kind: Kin, info ): SelectBranch<AllMedia, Kin> ``` We select a certain kind, and know which return type to expect. ```typescript createMedium('lp', { /* tbd */ }) // returns LP! createMedium('cd', { /* tbd */ }) // returns CD! ``` Now, we want to focus on the missing information. Remember, we want to add everything that's necessary to create a full medium, except for `id`, which is auto-generated by `createMedium`, or `kind`, which we already defined. ### Exclude This means that we need to pass objects that look like this: ```typescript type CDInfo = { title: string, description: string, tracks: number, duration: number } type LPInfo = { title: string, description: string, sides: { a: { tracks: number, duration: number }, b: { tracks: number, duration: number } } } ``` But we don't want those types to be maintained, we want to have them auto-generated. The first thing we want to take care of is knowing which keys of our object we actually need. And the best way to do this is by knowing wich keys we don't need: `kind` and `id`. ```typescript type Removable = 'kind' | 'id' ``` Good. Now we need to filter all property keys that are not in this set of keys. For that, we create another distributive conditional type. It looks very similar to `Extract`, but resolves differently. ```typescript type Remove<A, B> = A extends B ? never : A ``` It reads that if the type A is part of B, remove it (never), otherwise keep it. Let's see what happens if we use all keys of `CD` and distribute the union over the `Remove` type. Remember, a conditional of a union is like a union of conditionals. ```typescript // First our keys type CDKeys = keyof CD // equal to type CDKeys = 'id' | 'description' | 'title' | 'kind' | 'tracks' | 'duration' // now for the keys we actually want type CDInfoKeys = Remove<CDKeys, Removable> // equal to type CDInfoKeys = Remove<'id' | 'description' | 'title' | 'kind' | 'tracks' | 'duration', 'id' | 'kind'> // a conditional of a union // is a union of conditionals type CDInfoKeys = Remove<'id', 'id' | 'kind'> | Remove<'description', 'id' | 'kind'> | Remove<'title', 'id' | 'kind'> | Remove<'kind', 'id' | 'kind'> | Remove<'tracks', 'id' | 'kind'> | Remove<'duration', 'id' | 'kind'> // substitute type CDInfoKeys = ('id' extends 'id' | 'kind' ? never : 'id') | ('description' extends 'id' | 'kind' ? never : 'description') | ('title' extends 'id' | 'kind' ? never : 'title') | ('kind' extends 'id' | 'kind' ? never : 'kind') | ('tracks' extends 'id' | 'kind' ? never : 'tracks') | ('duration' extends 'id' | 'kind' ? never : 'duration') // evaluate type CDInfoKeys = never | 'description' | 'title' | never | 'tracks' | 'duration' // remove impossible types from the union type CDInfoKeys = 'description' | 'title' | 'tracks' | 'duration' ``` Wow, what a process! But we got one step closer to the result we expect. The `Remove` type is built-in in TypeScript and called `Exclude`. The definition is exactly the same, and it's description says that it excludes types from A which are in B. This is what just happened. ### Omit We now have to take this new set of keys, that is a subset of the original set of keys, and create an object type with the new keys. And also, the new keys need to be of the type of the original object. This sounds a lot like a mapped type, doesn't it? Remember `Pick`? Pick runs over a set of keys, and selects the type from the original property type. This is exactly what we're looking for. ```typescript type CDInfo = Pick< CD, Exclude<keyof CD, 'kind' | 'id'> > ``` How do we read this new type? We *pick* from *CD* all *keys of CD*, but exclude *kind* and *id*. The result is the type we originally invisioned. Once again, generic types behave like functions. They have parameters, have an output, and are composable. Reading this type might feel like a little tongue-twister. That's why TypeScript has a built-in type for exactly this combination of `Pick` and `Exclude`, called `Omit`. ```typescript type CDInfo = Omit<CD, 'kind' | 'id'> ``` We are very far with our types, the last step is to compose everything in our `createMedium` function. To succesfully omit *kind* and *id* from our medium types, we need to pass the selected branch to *Omit*. Another helper type makes this a bit more readable. ```typescript type RemovableKeys = 'kind' | 'id' type GetInfo<Med> = Omit<Med, RemvoableKeys> declare function createMedium< Kin extends MediaKinds >( kind: Kin, info: GetInfo<SelectedBranch<AllMedia, Kin>> ): SelectBranch<AllMedia, Kin> ``` And that's it! Now TypeScript prompts us only for the properties that are missing, We don't have to specifiy redundant information, and get autocomplete and type safety when using our `createMedium` function. ### A set of helper types Being able to compose generics and distributive conditional types allows for a set of smaller, single-purpose helper types that can be assembled for different scenarios. This allows us to define type behaviour without maintaining too many types. Focus on the model, describe behaviour with helpers. ## Lesson 41: The infer keyword When working efficently with TypeScript, we want to keep type maintenance as low as possible. Types should help us being productive, after all, not stand in the way of what we try to achieve. Up until now we stuck to a clear workflow: Model data, describe behaviour. We don't want too much time spending with type maintenance if we can create types dynamically from other types. There are times however, when we are not sure how our model will look like. Especially during development, we things can change. Data can be added and removed, and the overall shape of an object is in flux. This is okay, this is the flexibility JavaScript is known for! Think of extending our e-Commerce admin application with a function that creates users that are allowed to read and modifiy orders, products, and customers. The function might look something like this: ```typescript // a userId variable counting up... not safe // but we are in development mode let userId = 0 function createUser(name, roles) { return { userId: userId++, name, roles, createdAt: new Date() } } ``` Pretty straightforward. Two generated properties and the others are just added to the object. Notice the abscences of types! Later on, our function might get more concrete. The roles are divided between - admin, which are allowed to read and modify everything - maintenance, allowed to modify products - shipping, allowed to read orders to get the necessary info to dispatch them ```typescript function createUser( name: string, role: 'admin' | 'maintenace' | 'shipping', isActive: boolean ) { return { userId: userId++, name, role, isActive, createdAt: new Date() } } ``` But this is just another mutation. Input types get more concrete, the return type adapts to the changes. This also means that we always would have to maintain a type `User` if we want to continue work with users in a type-safe environment. ### Infer the return type It would help a lot if we could *name* the type that gets returned by `createUser`. TypeScript can infer types through assignments. The variable's type takes on the shape of what's returned by the function. ```typescript // the type of user is the shape returned by createuser const user = createUser('Stefan', 'shipping', true) ``` We can put a name on this with the *typeof* operator: ```typescript /* type User = { userId: number, name: string, role: 'admin' | 'maintenace' | 'shipping', isActive: boolean, createdAt: Date } */ type User = typeof user ``` This gets us the `User` type, but at a very high price. We always have to call the function to obtain the shape of the return type. This is OK in this case, but what if we have add a database transaction storing the users. This leads to a lot of side effects. What we want is to retrieve the return type from the function signature. For situations like that, TypeScript allows to infer type variables in the extends clause of a conditional type. Let's create a `GetReturn` type that takes a function. Any function. For now, we want to check if the passed type is a valid function. ```typescript type GetReturn<Fun> = Fun extends (...args: any[]) => any ? Fun : never ``` We combine all possible arguments into an argument tuple (see rest parameters in lesson 37), we know that we have any return type. If we pass our function, we get the type of the function in return: ```typescript type Fun = GetReturn<typeof createUser> ``` Now we have the ability to infer types that are in this extends clause. This happens with the `infer` keyword. We can choose a type variable, and return this type variable. ```typescript type GetReturn<Fun> = Fun extends (...args: any[]) => infer R ? R : never ``` With `infer R` we say that no matter what return type of this function comes, we store it in the type variable `R`. If we have a valid function, we return `R` as type. ```typescript /* type User = { userId: number, name: string, role: 'admin' | 'maintenace' | 'shipping', isActive: boolean, createdAt: Date } */ type User = GetReturn<typeof createUser> ``` Zero maintenance, always correct types. This helper type is available in TypeScript as `ReturnType<Fun>`. ### Helper types Helper types like `ReturnType` are essential if we construct functions and libraries where we care more about the behaviour and interconnection of parts rather than the actual types themselves. Storing and retrieving objects from a database, create objects based on a schema, that kind of thing. With the `infer` keyword we get a lot of flexibility to get types where we don't know what we are dealing with, yet. For example, a simple type that allows us to retrieve the resolved value of a Promise: ```typescript type Unpack<T> = T extends Promise<infer Res> ? Res : never type A = Unpack<Promise<number>> // A is number ``` Or a type that flattens an array, so we get the type of the array's values. ```typescript type Flatten<T> = T extends Array<infer Vals> ? Vals : never type A = Flatten<Customer[]> // A is Customer ``` TypeScript has a couple more built-in conditional types that use inference. One is `Parameters`, which collects all arguments from a function in a tuple. ```typescript type Parameters<T> = T extends (...args: infer Param) => any ? Param : never /* A is [ string, "admin" | "maintenace" | "shipping", boolean ] */ type A = Parameters<typeof createUser> ``` Others are - `InstanceType`, getting the type of the created instance of class's constructor function. - `ThisParameterType`, if you use callback functions that bind `this`, you can get the bound type in return. - `OmitThisParameterType`, uses `infer` to return a function signature without the `this` type. This is handy if your app doesn't care about the bound `this` type and needs to be more flexible in passing functions. There's one thing types with the `infer` keyword have in common: They are low-level utility types that help you glue parts of your code together with ease. This is behaviour defined in a type, and allows for very generic scenarios where your code has to be flexible enough to deal with unknown expectations. ## Lesson 42: Working with null In chapter 4 we learend that `undefined` and `null` are parts of every set in the type space, unless we set the flag `strictNullChecks`. This removes `undefined` and `null` and treats them as their own entities. This prompts TypeScript to throw red squigglies at us the moment we forget to handle nullish values. This has a great effect for the code we write on our own. If we use types as contract to pass data across functions, we can be sure that we have dealt with `null` and `undefined` already. The bitter truth is that nullish values can and will happen. At least at the point where our software has to work with input from the outside: 1. An element from the DOM we want to select 2. User input from an input field 3. Data we fetch asynchronously from a back-end Let's look at a very simplified `fetchOrderList` function, that does roughly the same as the one we've seen earlier in this chapter, but exclusively asynchronous. ```typescript declare function fetchOrderList( input: Customer | Product ): Promise<Order[]> ``` This function's contract tells us that we get a Promise that definitely returns a list of orders. The implicit message is that fetching orders has also succeeded, and that handling errors or nullish values has already happened. If we implement this function with `fetch` as we did in lesson 37, we see that we have a problem: `fetch` returns a Promise of *any*. Any being the top type that covers all and takes anything, also includes `null` and `undefined`. And `never`, if we take the possibility of an error into account. This means that at inside this function, we lose the information if the return value is actually defined. This means that we have to be more specific about the set of possible return values, especially since we say that we don't take nullish values into our sets with `strictNullChecks`. The *real* function head for `fetchOrderList` is much more like this: ```typescript declare function fetchOrderList( input: Customer | Product ): Promise<Order[] | null> ``` This is good. We add nullish values back to our sets and are explicit about it. This means that also are forced to check if values can be null. This makes our code much safer than before. ### NonNullable To handle null, we have two possibilities, as shown by a function called `listOrders` that prints all orders on the console. The first option is that we add null to the input arguments. ```typescript declare function listOrders(Order[] | null): void ``` This ensures that the `listOrders` function is compatible with the output from `fetchOrderList`. Null-checks have to be done inside `listOrders`. The other option is to make sure that we never pass nullish values to `listOrders`. This means that we have to do null checks before: ```typescript declare function listOrders(Order[]): void ``` In any case, we will have to do a check for null. And most likely not only for lists of orders, but also for lists of products, lists of customers, etc. This calls for a generic helper function, that does check if an object is acutally available. ```typescript declare function isAvailable<Obj>( obj: Obj ): obj is NonNullable<Obj> ``` This generic function binds to the type variable `Obj`. So far, `Obj` can be anything. We don't have any type constraints and don't want any type constraints. `isAvailable` should work with everything. But, the result shall ensure that we don't have any nullish values. That's why we use the built-in utility type `NonNullable`, which removes `null` and `undefined` from our set of values. `NonNullable` is defined as follows: ```typescript type NonNullable<T> = T extends null | undefined ? never : T ``` `NonNullable` is a distributive, conditional type. If we pass a union where we explicitly set `null` or `undefined`, we can make sure that we remove this value types again and continue with a narrowed down set. This is the implementation: ```typescript function isAvaialble<Obj)>( obj: Obj ): obj is NonNullable<obj> { return typeof obj !== 'undefined' && obj !== null } ``` This is the helper function in action: ```typescript // orders is Order[] | null const orders = await fetchOrderList(customer) if(isAvailable(orders)) { //orders is Order[] listOrders(orders) } ``` It's recommended to think about your nullish values early on. Keep the core of your application null-free, and try to catch any possible side-effect as sson as possible. ### Low-level utilities TypeScript's built-in conditional types help a lot if you work on very low-level utility functions that you can re-use over and over in your application. Same goes for our own utility types that we declared in the previous lesson. `fetchOrderList` is a very specific function, now think of a much more generic function, and about some possible processes. First, fetching from a database. ```typescript type FetchDBKind = 'orders' | 'products' | 'customers' type FetchDBReturn<T> = T extends 'orders' ? Order[] : T extends 'products' ? Products[] : T extends 'customers' ? Customers[] : never declare function fetchFromDatabase< Kin extends FetchKind >( kind: Kin ): Promise<FetchDbReturn<Kin>| null> ``` Processing anything that we fetched and making sure we get the right process function for this. ```typescript function process<T extends Promise<any>>( promise: T, cb: (res: Unpack<NonNullable<T>>) => void ): void {) promise.then(res => if(isAvailable(res)) { cb(res) } ) } ``` This allows us to safely `listOrders` to a function where the results can be ambiguous. ```typescript process( fetchFromDatabase('orders'), listOrders ) ``` ## Recap Conditional types are perhaps the most unique thing about TypeScript's type system. And they come as a direct result of the enormous flexibility JavaScript has to offer for developers. This is what we learned. 1. Conditional types are a great tool to make direct conections between input types and output types. Something that gets lost in function overloads very early. 2. Still, we need a good and healthy mix of conditional types and function overloads to make sure that functions that have ambiguous results can be understood in a type safe manner. 3. We learned about the distributive nature of conditional types when being used with *naked* type parameters. A conditional type of union types is like a union type of conditional types. 4. This allows us to filter with the *never* type, reducing unions and being more explicit about what we expect. 5. With helper types like `Pick`, `Extract`, and `Exclude` we can model behaviour to a set of data, and make sure that no matter how the data changes, our processes are prepared for change. Less type maintenance, more type safety. 6. We learned about the *infer* keyword and how it can be used to extract types out of a much more complex, generic type. 7. We learned about working with null and the `NonNullable` type, and realized how a good set of low-level primities can allow us to make our own code more generic and flexible, without losing any type safety. Conditional types become an invaluable member of your typing skills. We will be able to create strong typings with just a few lines of code, and can focus exclusively on writing JavaScript code. As we are going to see in the next chapter. ## Interlude: lib.d.ts As TypeScript is getting released multiple times per year, and JavaScript has a fixed update once a year, it is almost impossible to keep track of all the built-in utilities and helpers that are provided by TypeScript itself. One good source of looking at type definitions is by -- well -- looking at the source itself! TypeScript comes with a lot of library code, defining all the surroundings of JavaScripts that we can't work without: The `Array` object, the `Object` object. `Number`, `String`, you name it. To access the library, open the `lib` folder in your TypeScript installation. Or quicker: Select `Go to definition` if you right-click on a built-in type in your editor. This will open the respective file, but will also show you an extensive list of files that are split by version and environment. Those types are an intersting read. You will see what's defined in the DOM, you will see language features by EcmaScript version. This not only allows you to see how the TypeScript team uses TypeScript on their own to define what's happening in JavaScript. This also allows you to see the behaviour of new EcmaScript features in a very condensed, but extremly well documented manner. For example, in ES2018 we got a *finally* clause for Promises. It allows you to specify a task that is executed no matter if the promises resolves to a value or is rejected. This is what the *lib.es2018.promise.d.ts* file has to say: ```typescript /** * Represents the completion of an asynchronous operation */ interface Promise<T> { /** * Attaches a callback that is invoked when * the Promise is settled (fulfilled or rejected). The * resolved value cannot be modified from the callback. * @param onfinally The callback to execute when the * Promise is settled (fulfilled or rejected). * @returns A Promise for the completion of the callback. */ finally(onfinally?: (() => void) | undefined | null) : Promise<T> } ``` Due to declaration merging, this small snippet is added to the complete specification of *Promise*. That's how we don't have to read the full specification, but can focus on what's new. Handy!