# Chapter 4: Union and Intersection Types We've come very far with TypeScript. We heard about the tooling aspect, type inference and control flow analysis, and you know how to type objects and functions effectively. With what we have learned, we are able to write pretty complex applications and get most likely good enough tooling out of it to get us through the day. But JavaScript *is* special. The flexible nature of JavaScript that allows for easy to use programming interfaces is, frankly, hard to sum up in regular types. This is why TypeScript offers a lot more. Starting with this chapter, we go deep into the depths of TypeScript's type system. We will learn about the set theory behind TypeScript, and how thinking in unions and interesections will help us getting even clearer and more comprehensible type support. This is where TypeScript's type system really shines, and starts becoming much more powerful than what we know from traditional programming languages. It's going to be an adventureous ride! To illustrate the concepts of union and intersection types, we work on a page for tech events: Meetups, conferences, webinars. Events that are similar in their nature, but distinct enough to be treated differently. ## Lesson 22: Modellng data Imagine a website that lists different tech events: 1. Tech conferences. This is where people meet at a certain *location*, and listen to a couple of *talks*. Conferences usually cost something, so they have a *price*. 2. Meetups. Smaller in scale, meetups are very similar to conferences from a data perspective. They also happen at a certain *location* with a range of *talks*, but compared to tech conferences they are usually *free*. Well, at least in our example. 3. Webinars. Instead of happening on-site, webinars are on-line. They don't need a location, but an URL where people can watch the webinar in their browser. They can have a price, but can also be free. Compared to the other two event types, webinars only feature one *talk*. All tech events have common properties, like a date, a description, a maximum number of attendees and a current RSVP count. We also get a string identifier in the property *kind*, where we can distinguish between conferences, webinars and meetups. In our app, we're working with that kind of data *a lot*. We get a list of tech events as JSON from a back-end, and when we add new events to a list, or want to retreive their properties to display them in a UI. To make live easier for us as developers, much less prone to errors, we want to spend some time modelling this data as TypeScript types. With that, we not only get proper tooling, but also red squiggly lines should we forget something. Let's start with the easy part. Every kind of tech event has some sort of talks. A talk has a title, an abstract, and a speaker. We keep the speaker simple for now and represent them with a simple string. The type for a talk looks like this: ```typescript type Talk = { title: string, abstract: string, speaker: string } ``` With that in place, we can do a type for conferences: ```typescript type Conference = { title: string, description: string date: Date, capacity: number, rsvp: number, kind: string, location: string, price: number, talks: Talk[] } ``` A type for meetups, where `price` is a string (`"free"`) instead of a number: ```typescript type Meetup = { title: string, description: string date: Date, capacity: number, rsvp: number, kind: string, location: string, price: string, talks: Talk[] } ``` And a type for webinars, where we only have one talk, and we don't have a physical location but a URL to host the event: ```typescript type Webinar = { title: string, description: string date: Date, capacity: number, rsvp: number, kind: string, url: string, price?: number, talks: Talk } ``` Also, you see that types are optional. With those four types in place, we already modelled a good part of the possible data we can get from the back-end. And as we can see, some parts have a common shape within all three event types, and other parts are subtly, or even entirely different. ### Intersection types The first thing we realise is that there are lots of similar properties. Properties that also should stay the same, the basic shape of a `TechEvent`. With TypeScript, we have a possibility to extract that shape and combine it with properties special to our concrete single types. Let's first create a `TechEventBase` type that contains all properties that are the same in all three event types. ```typescript type TechEventBase = { title: string, description: string date: Date, capacity: number, rsvp: number, kind: string } ``` Then, let's refactor the original three types to combine `TechEventBase` with the specific properties of each type. ```typescript type Conference = TechEventBase & { location: string, price: number, talks: Talk[] } type Meetup = TechEventBase & { location: string, price: string, talks: Talk[] } type Webinar = TechEventBase & { url: string, price?: number, talks: Talk } ``` We call this concept *intersection types*. We read the `&` operator as *and*. We combine the properties from one type A with that of another type B, much like extending classes. The result is a new type with the properties of type A *and* type B. The immediate benefit that we get is that we can model common properties in one place, which makes updates and changes a lot easier. Also, the actual difference between types becomes a lot clearer and easier to read. Each sub-type has just a couple of properties we need to take care of, instead of the full list. ### Union types But what happens if we get a list of tech events, where each entry can bei either a webinar, or a conference, or a meetup. Where we don't know exactly what entries we get, only that they are of one of the three event types. For situations like that, we can use a concept called *union types*. With union types we can model exactly the following scenario: Defining a `TechEvent` type, that can be either a `Webinar`, or a `Conference`, or a `Meetup`. Or in code: ```typescript type TechEvent = Webinar | Conference | Meetup; ``` We read the pipe operator `|` as *or*. What we get is a new type. A type that tries to encompass all possible properties that we get from the types we set in union. The new type can access the following properties: - `title`, `description`, `date`, `capacity`, `rsvp`, `kind`. The properties all three types have in common, with their original primitive type. This is what the shape of `TechEventBase` gives us. - `talks`. This property can be either a single `Talk`, or an array `Talk[]`. Its new type is `Talk | Talk[]`. - `price`. The property `price` is also available in all three original object types, but its own type is different. `price` can be either of `string` or `number`, and according to `Webinar`, it can be optional. To safely work with price, we have to do some checks within our code: We have to check if it's available, and then we have to do `typeof` checks to see if we're dealing with a `number` or a `string`. Working with `price` and `talks` might look something like this: ```typescript function printEvent(event: TechEvent) { if(event.price) { // price exists! if(typeof event.price === 'number') { // we know that price is a number console.log('Price in EUR: ', event.price) } else { // we know that price is a string, so the // event is free! console.log('It is free!') } } if(Array.isArray(event.talks)) { // talks is an array event.talks.forEach(talk => { console.log(talk.title) }) } else { // it's just a single talk console.log(event.talks.title) } } ``` Does this structure remind you of something? Back in chapter two we learned about the concept of "control flow", and narrowing down types with type guards. This is exactly what's happening here. Since the type can take on different shapes, we can use type guards (*if* statements) to narrow down the *union* type to its single type. Please note that we are moving between the *union* types of the respective properties *price* and *talks*. All other information of the original types `Webinar`, `Conference`, and `Meetup` that can't be unified (like location and URL) are dropped from the shape of the union. We need some more information to narrow down to the original object shapes. ## Lesson 23: Moving in the type space Before we continue, let's review real quick what we've just learned. We learned about *intersection* types, the way to combine two or more types into one, much like extending from an object type. And we learned about *union* tyoes, a way to extract the lowest common denominator of a set of types. But why do we call them *intersection* and *union* types? ### Set theory To find out, we need to review what types actually are. In his book "Programming with Types", [Vlad Riscutia](https://www.manning.com/books/programming-with-types) defines types as follows: > A type is a classification of data that defines the operations that can be done on that data, the meaning of the data, and the set of allowed values. The part we want to focus on is the "set of allowed values". This is something that we already experienced when working with types. Once a variable has a certain type annotation, TypeScript only allows a specific set of values to be assigned. Type *string* only allows for strings to be assigned, *number* only allows for numbers to be assigned. Each types deals with a distinct set of values. When we think further, we can put those sets in a hierarchy. The types *any* and *unknown* encompass the whole set of all available values. They are known as *top* types, as they are on the very top of the hierarchy. ![Top types, including all other types](https://i.imgur.com/oggzNbc.jpg) Primitive types such as *boolean*, *number* or *string* are one level below *any* and *unknown*. They cluster the set of all available values into distinct sets of specific values: All boolean values, all numbers, all strings. ![Primitive and complex type sets](https://i.imgur.com/y0FBpC8.jpg) Those sets are distinct. They don't share any common values. If we now build a union type `string | number`, we allow for all values that are either from the set *string* or the set *number*. Which means that we get a union set of possible values. ![A union of numbers and string](https://i.imgur.com/6YobSCq.jpg) Vice versa, if we build a intersection type `string & number`, we get an empty intersection set as they don't share any common values. This is also where the term *narrowing down* comes from. We want to have a narrower set of values. If our type is *any*, we can do a *typeof* check to narrow down to a specific set in the type space. We move from a top type *down* to a *narrower* set of values. ### Object sets With primitive types it's easy, but it gets a lot more fun if we consider object types. Take these two types for example: ```typescript type Name = { name: string } type Age = { age: number } ``` Since we have a structural type system, an object like ```typescript const person = { name: 'Stefan Baumgartner', city: 'Linz' } ``` is a valid value of type `Person`. This object ```typescript // In my midlife crisis, I don't use semicolons // ... just like the cool kids const midlifeCrisis = { age: 38, usesSemicolons: false } ``` is a valid value of type `Age`. This object ```typescript const me = { name: 'Stefan Baumgartner', age: 38 } ``` is compatible with both `Age` and `Name`. However, we can't assign every value of type `Age` to a type `Name`, as the sets are distinct enough to not have any common values. Once we define the union type `Age | Name`, both `midlifeCrisis` and `person` are compatible with the newly created type. The set gets wider, the number of compatible values gets bigger. But also, we lose clarity. Vice versa, a intersection `type Person = Age & Name` combines both sets. Now we need all properties from type `Age` and type `Name`. ![An intersection of Name and Age](https://i.imgur.com/j05QPvG.jpg) With that only the variable `me` becomes compatible with the newly generated type. The intersection is a subset of both sets `Age` and `Name`. Smaller, narrower, and we have to be more explicit about our values. > Formally speaking, all values from type `A` are compatible with type `A | B`, and all values from type `A & B` are compatible with type `B`. ### Value types Let's take this concept of narrowing and widening sets even further. We now know that we can have all available values and narrow them down to their primitive types. We can narrow down the complex types, like the set of all available objects, to smaller sets of possible objects defined on their property keys. Can we get even smaller? We can! We can narrow down primitive types to values. Turns out each specific value of a set is its own type. A **value type**. ![And finally, value types](https://i.imgur.com/67uNoAt.jpg) Let's look at the string `'conference'` for example. ```typescript let conf = 'conference' ``` Our variable `conf` is compatible with a couple of types: ```typescript let withTypeAny: any = 'conference' // 👍 let withTypeString: string = 'conference' // 👍 // but also: let withValueType: 'conference' = 'conference' // 👍 ``` You see that the set gets narrower and narrower. Type `any` selects all possible values, type `string` all possible strings. But type `'conference'` selects the specific string `'conference'`. No other strings are compatible. TypeScript is aware of value types when assigning primitive values: ```typescript // type is string, because the value can change let conference = 'conference' // type is 'conference', because the value can't // change anymore. const conf = 'conference' ``` Now that we've narrowed down the set to value types, we can create wider custom sets again. Let's get back to our example from before. We have three different types of tech events: conferences, webinars, and meetups. When our back-end sends along information on which kind of events we are dealing with, we can create a custom union type: ```typescript type EventKind = 'webinar' | 'conference' | 'meetup' ``` With that, we can make sure that we don't assign any values that are not intended, we rule out typos, and other mistakes. ```typescript // Cool, but not possible let tomorrowsEvent: EventKind = 'concert' ^^^^^^^^^^^^^^ ``` The value sets of primitive types are technically infinite. We would never be reasonably able to express the full spectrum of string or number in a custom type. But we can take very specific slices out of it when it comforts our data. > When we are deep in TypeScript's type system, we do a lot of set widening and narrowing. Moving around in sets of possible values is key to define clear, yet flexible types that give us top of the class tooling. ## Lesson 24: Working with value types Let's incorporate our newly found knowledge on value and union types to our tech event data structure. In lesson 1, we deducted a `TechEventBase` type that includes all common properties of each tech event: ```typescript type TechEventBase = { title: string, description: string date: Date, capacity: number, rsvp: number, kind: string } ``` The last property of this type is called `kind` and holds information on the kind of tech event we are dealing with. The type of `kind` is `string` at the moment, but we know more. We know that this type can only take three distinct values: ```typescript type TechEventBase = { title: string, description: string date: Date, capacity: number, rsvp: number, kind: 'conference' | 'meetup' | 'webinar' } ``` That's already much better than the previous version. We are more secure against wrong values and typos. This has an immediate effect on what we can do with the combined union type `TechEvent`. Let's look at another function called `getEventTeaser`: ```typescript function getEventTeaser(event: TechEvent) { switch(event.kind) { case 'conference': return `${event.title} (Conference)` case 'meetup': return `${event.title} (Meetup)` case 'webinar': return `${event.title} (Webinar)` // Again: cool, but not possible case 'concert': ^^^^^^^^^ } } ``` TypeScript immediately errors, because the type `"concert"` is not comparable to type `"conference" | "meetup" | "webinar"`. Unions of value types are *brilliant* for control flow analysis. We don't run into situations that can't happen, because our types don't support situations like that. All possible values of the set are taken care of. ### Discriminated union types But we can do more. Instead of putting a union of three value types at `TechEventBase`, we can move very distinct value types down to the three specifc tech event types. First, we drop `kind` from `TechEventBase`: ```typescript type TechEventBase = { title: string, description: string date: Date, capacity: number, rsvp: number, } ``` And add distinct value types to each specific tech event. ```typescript type Conference = TechEventBase & { location: string, price: number, talks: Talk[], kind: 'conference' } type Meetup = TechEventBase & { location: string, price: string, talks: Talk[], kind: 'meetup' } type Webinar = TechEventBase & { url: string, price?: number, talks: Talk, kind: 'webinar' } ``` On a first glance, everything stays the same. If you hover over the `event.kind` property in our switch statement, you see that the type for `kind` is still `"conference" | "meetup" | "webinar"`. Since all three tech event types are combined in one union type, TypeScript creates a proper union type for this property, just as we would expect. But underneath, something wonderful happens. Where before TypeScript just knew that some properties of the big `TechEvent` union type exist or don't exist, with a specific value type for a property, we can directly point to the surrounding object type. Let's see what this means for the `getEventTeaser` function: ```typescript function getEventTeaser(event: TechEvent) { switch(event.kind) { case 'conference': // We now know that I'm in type Conference return `${event.title} (Conference), ` + // suddenly I don't have to check for price as // TypeScript knows it will be there `priced at ${event.price} USD` case 'meetup': // We now know that we're in type Meetup return `${event.title} (Meetup), ` + // suddenly we can say for sure that this // event will have a location, because the // type tells us `hosted at ${event.location}` case 'webinar': // We now know that we're in type Webinar return `${event.title} (Webinar), ` // suddenly we can say for sure that there will // be a URL `available online at ${event.url}` default: throw new Error('Not sure what to do with that!') } } ``` Using value types for properties works like a hook for TypeScript to find the exact shape inside a union. Types like this are called *discriminated union types*, and a safe way to move around in TypeScript's type space. ### Fixating value types Discriminating unions are a wonderful tool when you want to steer your control flow into the right direction. But it comes with some gotchas when you are relying heavily on type inference (which you should). Let us define a conference object outside of what we get from the back-end. ```typescript const script19 = { title: 'ScriptConf', date: new Date('2019-10-25'), capacity: 300, rsvp: 289, description: 'The feelgood JS conference', kind: 'conference', price: 129, location: 'Central Linz', talks: [{ speaker: 'Vitaly Friedman', title: 'Designing with Privacy in mind', abstract: '...' }] }; ``` By our type signature, this would a perfectly fine value of the type `TechEvent` (or `Conference`). However, once we pass this value to the function `getEventTeaser`, TypeScript will hit us with red squiggly lines. ```typescript getEventTeaser(script19) ^^^^^^^^ ``` According to TypeScript, the types of `script19` and `TechEvent` are incompatible. The problem lies in type inference. The moment we assign this value to the variable `script19`, TypeScript tries to guess the correct type of each property value, and aims for the set it can be most sure it will work. As with `const` objects all properties are still variable, infered types are mostly strings and numbers for simple properties. This means the property `kind` in `script19` will not be infered as `'conference'`, but as `string`. And `string` is a much wider set of values than `'conference'`. For this to work, we need to tell TypeScript again that we are looking for the value type, not for its superset of types. We have a couple of possibilities to do that. First, let's do a left-hand side type annotation. ```typescript const script19: TechEvent = { // all the properties from before ... } ``` with that, TypeScript does a type-check right at the assignment. This way, the value `'conference'` for `kind` will be seen as the annotated value type, instead of the much wider string. Not only that, but TypeScript will also understand which sub-type of the discriminated type union we are dealing with. If you hover over `script19` in usage, you'll see that TypeScript will correctly understand this value as `Conference`. ![Declared as TechEvent, understood as Conference](https://i.imgur.com/5qXd56v.png) But we lose some of the conveniences we get when we rely on type inference. Most of all that we can leverge structural typing and work freely with objects that just need to be compatible with types rather than explicitly be of a certain shape. For scenarios like that, we can fixate certain properties by doing type-casts. One way would be to cast the type of property `kind` specifically to the value type: ```diff const script19 = { title: 'ScriptConf', date: new Date('2019-10-25'), capacity: 300, rsvp: 289, description: 'The feelgood JS conference', - kind: 'conference', + kind: 'conference' as 'conference', price: 129, location: 'Central Linz', talks: [{ speaker: 'Vitaly Friedman', title: 'Designing with Privacy in mind', abstract: '...' }] }; ``` That will work, but we lose some type safety as we could also cast `'meetup' as 'conference'`. Suddenly we don't know again which types we are dealing with, and this is something we want to avoid. Much better is to tell TypeScript that we want to see this value in its `const` context: ```diff const script19 = { title: 'ScriptConf', date: new Date('2019-10-25'), capacity: 300, rsvp: 289, description: 'The feelgood JS conference', - kind: 'conference', + kind: 'conference' as const, price: 129, location: 'Central Linz', talks: [{ speaker: 'Vitaly Friedman', title: 'Designing with Privacy in mind', abstract: '...' }] }; ``` This works just like assigning a primitive value to a `const` and fixate its value type. ![What we get with `as const`](https://i.imgur.com/MesSEVc.pngimg/ch-4-3-2.png) You can apply `const` context event to objects, casting all properties to their value types, effectively creating a value type of an entire object. As a side effect, the whole object becomes read-only. ## Lesson 25: Dynamic unions Consider the following function. We get a list of tech events, and want to filter them by a specific event type: ```typescript type EventKind = 'conference' | 'webinar' | 'meetup' function filterByKind( list: TechEvent[], kind: EventKind ): TechEvent[] { return list.filter(el => el.kind === kind) } ``` This function takes two arguments: *list*, the original event list, and *kind*, the kind we want to filter by. We return a new list of tech events. We make use of two types to get better type safety. One is `TechEvent` which we used a lot in the last lessons. The other one is `EventKind`, a union of all available value types for the property `kind`. With that union in place, we are allowed to only filter by event kinds listed in that union: ```typescript // a list of tech events we get from a back-end declare const eventList: TechEvent[] filterByKind(eventList, 'conference') // 👍 filterByKind(eventList, 'webinar') // 👍 filterByKind(eventList, 'meetup') // 👍 // 'concert' is not part of EventKind filterByKind(eventList, 'concert') // 💥 ``` This is a tremendous improvment for developer experience, but has some downfalls when our data is changing. ### Lookup types What if we get another event type to the existing list of event types, called `Hackathon`? A live, in-person coding event that might cost something, but has no talks. Let's define the new type: ```typescript type Hackathon = TechEventBase & { location: string, price?: number, kind: 'hackathon' } ``` And add `Hackathon` to the union of TechEvents: ```typescript type TechEvent = Conference | Webinar | Meetup | Hackathon ``` Immediately we get a disconnect between `EventKind` and `TechEvent`. We can't filter by `'hackathon'` even though it should be possible. ```typescript // this should be possible filterByKind(eventList, 'hackathon') // 💥 ``` One way to change this would be to adapt `EventKind` every time we change `TechEvent`. But this is a lot of effort, especially with growing or changing lists of data. What if, all of a sudden, in-person conferences are not a thing anymore? We want to keep the changes we make to our types as minimal as possible. For that, we need to create a connection between `EventKind` and `TechEvent`. You might have noticed that object types have a similar structure to JavaScript objects. Turns out, we have similar operators on object types as well. Just like we can access the property of an object by indexing it, we can access the type of a property by using the right index: ```typescript declare const event: TechEvent // accessing the kind property via the inde // operator console.log(event['kind']) // doing the same thing on a type level type EventKind = TechEvent['kind'] // EventKind is now // 'conference' | 'webinar' | 'meetup' | 'hackathon' ``` Since the union of `TechEvent` already combines all possible values of property types into unions, we don't need to define `EventKind` on our own anymore. Types like this are called *index access types* or *lookup types*. With lookup types we create our own system of connected types that produce red squiggly lines everywhere we didn't expect them. As a safe guard for our own, ever changing work. ### Mapped types Speaking of dynamically generated types, let's look at a function that groups events by their kind. ```typescript type GroupedEvents = { conference: TechEvent[], meetup: TechEvent[], webinar: TechEvent[], hackathon: TechEvent[] } function groupEvents( events: TechEvent[] ): GroupedEvents { const grouped = { conference: [], meetup: [], webinar: [], hackathon: [] }; events.forEach(el => { grouped[el.kind].push(el) }) return grouped } ``` The function creates a map, and then stores the original list of tech events in a new order, based on the event kind. Again, we face a similar problem as before. The type `GroupedEvents` is manually maintained. We see that we have four different keys based on the events that we work with, and the moment the original `TechEvent` union changes, we would have to maintain this type as well. Thankfully, TypeScript has a tool for situations like this as well. With TypeScript we have the possibility to create object types by running over a set of value types to generate property keys, and assigning them a specific type. In our case, we want the keys `hackathon`, `webinar`, `meetup`, and `conference` to be generated automatically and mapped to a `TechEvent` list by running over `EventKind`: ```typescript type GroupedEvents = { [Kind in EventKind]: TechEvent[] } ``` We call this kind of types *mapped types*. They're indicated by having no clear property names, but brackets to indicate a placeholder for eventual property keys. In our example, the property keys are generated by loopin over the union type `EventKind`. To visualize how this works, let's expand the mapped type ourselves in a couple of steps: ```typescript // 1. the original declaration type GroupedEvents = { [Kind in EventKind]: TechEvent[] } // 2. resolving the type alias. // we suddenly get a connection to tech event type GroupedEvents = { [Kind in TechEvent['kind']]: TechEvent[] } // 3. Resolving the union type GroupedEvents = { [Kind in 'webinar' | 'conference' | 'meetup' | 'hackathon']: TechEvent[] } // 4. extrapolating keys type GroupedEvents = { webinar: TechEvent[], conference: TechEvent[], meetup: TechEvent[], hackathon: TechEvent[], } ``` As we get from our original type! Mapped types are not only a convenience that allows us to write a lot less and get the same kind of tooling. We create a elaborate network of connected type information that allows us to catch errors the very moment our data changes. The moment we add another kind of events to our list of tech events, `EventKind` gets an automatic update and we get more information for `filterByKind`. Also, we know that we have another entry in `GroupedEvents`, and the function `groupEvents` won't compile because the return value lacks a key. And we get all this benefits without us doing anything. We just have to be clear with our types and create the necessary connections. Plus, maintaining types is a source of potential errors. Dynamically updating types helps. ## Lesson 26: Object keys and type predicates Our website not only lists events of different kinds, they also allow users to maintain lists of events they're interested in. For users, events can have different states: 1. Users can be *watching* events because they're interested in. With that, they can keep up to date on speaker announcements and more. 2. Users can be actively subscribed to events, meaning that they either plan to attend or already paid the entry fee. With that, they *rsvp*-ed to the event. 3. Users can have *attended* events that have been held in the past. With that list of events they want to keep track on video recordings, feedback, and slides. 4. Users can have *signed out* of events, meaning that they were either subscribed to, but changed their mind, or that they just don't want to see that event in their lists anymore. Our application keeps track of those events as well. As always, we want to model our data first. As we don't want to change our existing types, but want a quick way to access all four categories, we create another object that serves as map to each category. The type for this object looks like this: ```typescript type UserEvents = { watching: TechEvent[], rvsp: TechEvent[], attended: TechEvent[], signedout: TechEvent[], } ``` Now for some operations on this object. ### keyof We want to give users the option to filter their events. First by category -- watching, rsvp, attended, and signed out. Second, and optionally, by the kind of event: Conference, meetup, webinar, or hackathon. The function we want to create accepts three arguments: 1. The `userEventList` we want to filter. 2. The `category` we want to select. This is equal to one of the keys of the `userEventList` object. 3. Optionally, a string of the set `EventKind` that allows us to filter even further. The first filter operation is quite simple. We want to access one of the lists via the index access operator, e.g. `userEventList['watching']`. So for the type of the `category` we create a union type that includes all keys of `userEventList`. ```typescript type UserEventCategory = 'watching' | 'rsvp' | 'attended' | 'signedoff' function filterUserEvent( userEventList: UserEvents, category: UserEventCategory, filterKind?: EventKind ) { const filteredList = userEventList[category] if (filterKind) { return filteredList.filter(event => event.kind === filterKind) } return filteredList } ``` This works, but we face the same problems as we did in the previous lesson: We maintain types manually. This is prone to errors and typos. Problems of that kind that are hard to catch. I guess you didn't realize that I made a mistake by using the value type `signedoff` in `UserEventCategory` which isn't a key in `UserEvents`. That would be `signedout`. We want to create types like this dynamically, and TypeScript has an operator for that. With `keyof` we can get the object keys of *every* type that we define. And I mean *every*. We can use *keyof* even with value types of the string set and get all string functions. Or with an array and get all array operators: ```typescript // 'speaker' | 'title' | 'abstract' type TalkProperties = keyof Talk // number | 'toString' | 'charAt' | ... type StringKeys = keyof 'speaker' // number | 'length' | 'pop' | 'push' | ... type ArrayKeys = keyof [] ``` The result is a union type of value types. We want the keys of our `UserEvents`, so this is what we do: ```typescript function filterUserEvent( userEventList: UserEvents, category: keyof UserEvents, filterKind?: EventKind ) { const filteredList = userEventList[category] if (filterKind) { return filteredList.filter(event => event.kind === filterKind) } return filteredList } ``` The moment we update our `UserEvent` type, we also know which keys we have to expect. So if we remove something, occurences where we use that removed key get red squiggly lines. If we add another key, TypeScript will give us proper autocomplete for it. ### type predicates Let's assume that `filterUserEvents` is not only within our application, but also available outside. Other developer teams in our organisation can access the function, and they might not use TypeScript to get their job done. For them, we want to catch some possible errors upfront, while still retaining our type safety. From both filter operations, the category filter is the problematic one, as it could access a key that is not available in `userEventList`. To make it still type safe for us and more flexible to the outside, we accept that `category` is not a subset of string, but the whole set of strings: ```typescript function filterUserEvent( list: UserEvents, category: string, filterKind?: EventKind ) { // ... tbd } ``` But before we access the category, we want to check if this is a valid key in our list. For that, we create a helper function called `isUserEventListCategory`: ```typescript function isUserEventListCategory( list: UserEvents, category: string ) { return Object.keys(list).includes(category) } ``` And apply this check to our function: ```typescript function filterUserEvent( list: UserEvents, category: string, filterKind?: EventKind ) { if(isUserEventListCategory(list, category)) { const filteredList = list[category] ^^^^^^^^^^^^^^ if (filterKind) { return filteredList.filter(event => ^^^^^ event.kind === filterKind) } return filteredList } return list } ``` This is enough safety to not crash the program if we get input that doesn't work for us. But TypeScript (especially in strict mode) is not happy with that. We lose all connections to UserEvents, and `category` is still a string. So how can we on a type level be sure that we access the right properties? This is were *type predicates* come in. Type predicates are a way in TypeScript to add more information to control flow analysis. We can extend the possibilites of narrowing down by telling TypeScript that if we do a certain check, we can be sure our variables are of a certain type: ```typescript function isUserEventListCategory( list: UserEvents, category: string ): category is keyof UserEvents { // the type predicate return Object.keys(list).includes(category) } ``` Type predicates work with functions that return a boolean. If this function evaluates to true, we can be sure that `category` is a key of `UserEvents`. This means that in the *true*-branch of the if statement, TypeScript knows the type better. We narrowed down the set of `string` to a smaller set `keyof UserEvents`. ## Lesson 27: Down at the bottom: never With all that widening and narrowing of sets, even down to single values being a type, we have to ask ourselves: Can we get even narrower? Yes, we can. There's one type that's at the very bottom of the type hierarchy. One type that is an even smaller set than a set with one value. The type without values. The empty set: Never. ### never in control flow analysis Never behaves pretty much like the anti-type of *any*. Where *any* accepts all values and all operations on that values, *never* does not accept a single value. It's impossible to assign a value, and of course there are no operations that we can do on a type that is *never*. So how does a type with no values feel like, when we are working with it? Well, we briefly touched on it already, it was hidden in plain sight. Let's go back a couple of lessons and remember what we did when writing the `getEventTeaser` function, now with the `Hackathon` type included: ```typescript function getEventTeaser(event: TechEvent) { switch(event.kind) { case 'conference': return `${event.title} (Conference), ` + `priced at ${event.price} USD` case 'meetup': return `${event.title} (Meetup), ` + `hosted at ${event.location}` case 'webinar': return `${event.title} (Webinar), ` `available online at ${event.url}` case 'hackathon': return `${event.title} (Hackathon)` default: throw new Error('Not sure what to do with that!') } } ``` This switch statement runs through all the value types within the `EventKind` union type: `'conference' | 'meetup' | 'webinar' | 'hackathon'`. With every `case` statement in our switch, TypeScript knows to take one value type away from this list. When we check for `'conference'` already, it can't be checked again later on. Once this list is exhausted, we have no more values in our set left. The list is empty. This is the `default` branch in our switch statement. But, if we checked for all values in our list, why would we run into a `default` brach anyway? Wouldn't that be errornous behaviour? Exactly! This is highly errornous, as we indicate by throwing a new error right away! Running into the `default` branch can never happen. *Never*! There it was, the *never* word. So this is what type `never` is all about. It indicates the cases where we run into situations that aren't supposed to happen, telling us that we shall be very careful as our variables most likely don't contain the values we expect. If you take the example from above, enter `event` in the first line of the `default` branch and hover over it, TypeScript will show you exactly that. ![The list is exhausted, event is never](https://i.imgur.com/Hmds9R9.png) Any operation on `event` other than being part of an error thrown will cause compiler errors. This is a sitatuation that should never happen, after all! ### Preparing for dynamic updates Right now, our `getEventTeaser` function deals with all entries from `EventKind`. In case a value comes in that isn't part of the union type, we throw an error. This is great, but only works if we handle all possible cases. What if we haven't exhausted our entire list, yet? Let's remove `'hackathon'` for now: ```typescript function getEventTeaser(event: TechEvent) { switch(event.kind) { case 'conference': return `${event.title} (Conference), ` + `priced at ${event.price} USD` case 'meetup': return `${event.title} (Meetup), ` + `hosted at ${event.location}` case 'webinar': return `${event.title} (Webinar), ` `available online at ${event.url}` default: throw new Error('Not sure what to do with that!') } } ``` In the default branch, `event.kind` is now `'hackathon'`, but we aren't dealing with it, we just throw an errors. This is somewhat right as *we are not sure what to do with that*, but it would be a lot nicer if TypeScript give us information that we forgot something. We want to exhaust our entire list, after all. For that, we want to make sure that at the end of a long switch-case statement, or in else branches that shouldn't occur, the type of `event` is definitely never. Let's create a utility function that throws the error. But instead of sending just a message, we also want to send the culprit that eventually caused that error. The clue: The type of this culprit is *never*. ```typescript function neverError( message: string, token: never // the culprit ) { return new Error( `${message}. ${token} should not exist` ) } ``` We substitute the `neverError` function with the actual error throwing in our switch-case statement: ```typescript function getEventTeaser(event: TechEvent) { switch(event.kind) { case 'conference': return `${event.title} (Conference), ` + `priced at ${event.price} USD` case 'meetup': return `${event.title} (Meetup), ` + `hosted at ${event.location}` case 'webinar': return `${event.title} (Webinar), ` `available online at ${event.url}` default: throw neverError( 'Not sure what to do with that', event ^^^^^ ) } } ``` And immediately TypeScript's type checking powers kick in. At this point, `event` could potentially be a hackathon. We're just not dealing with that. TypeScript gives us a red squiggly and tells us that we can't pass some value to a function that expects `never`. After we add `'hackathon'` to the list again, TypeScript will compile again, and all our exhaustive checks are complete. ```typescript function getEventTeaser(event: TechEvent) { switch(event.kind) { case 'conference': return `${event.title} (Conference), ` + `priced at ${event.price} USD` case 'meetup': return `${event.title} (Meetup), ` + `hosted at ${event.location}` case 'webinar': return `${event.title} (Webinar), ` `available online at ${event.url}` case 'hackathon': return `even that: ${event.title}` default: throw neverError( 'Not sure what to do with that', event // no complaints ) } } ``` With *never* we get a safe guard that can be used for situations that could occur, but should never occur. Especially when dealing with sets of values that get wider and narrower as we code our applications. *never* is the bottom type of all other types, and will be a handy tool in the next chapters. ## Lesson 28: Undefined and null Before we close this chapter, we have to talk about two special value types, that you will catch sooner or later in your applications: `null` and `undefined`. Both `null` and `undefined` denote the absence of a value. `undefined` tells us that a variable or property has been declared, but no value has been assigned. `null` on the other hand is an *empty value* that can be assigned to clear a variable or property. Both values are known as *bottom values*, values that have no actual value. Douglas Crockford [once said](https://www.youtube.com/watch?v=99Zacm7SsWQ) that there is much discussion in the programming language community if a programming language should even have bottom values. Nobody has the opinion that there need to be two of them. ### Undefined and null in the type space `undefined` and `null` are somewhat special in TypeScript. Regularly, both values are part of each set of types. ![The type number with undefined and null](https://i.imgur.com/iKq5wvk.png) This is because JavaScript behaves that way. The moment we declare a variable, it is set to `undefined`. Programatically, we can set variables to `null` or `undefined`. But, this brings along some problems. Let's look at this simple example: ```typescript // Let's define a number variable let age: number // I'm getting one year older! age = age + 1 ``` This is valid TypeScript code. We declare a number, and add another number value to it. The problem is that his brings us in values we would not expect. The result of this operation is `NaN`, because we are adding 1 to `undefined`. Technically, the result is again of type `number`, but just not what we expect! It can be worse. Let's go back to our tech event example. We want to create an HTML representation of one of our events and append it to a list of elements. We create a function that runs over the common properties and returns an string: ```typescript function getTeaserHTML(event: TechEvent) { return `<h2>${event.title}</h2> <p> ${event.description} </p>` } ``` We use this function to create a list element, which we can add to our list of events: ```typescript function getTeaserListElement(event: TechEvent) { const content = getTeaserHTML(event) const element = document.createElement('li') element.classList.add('teaser-card') element.innerHTML = content return element } ``` A bit rough, but it does the trick. Now, let's add this element to a list of existing elements: ```typescript function appendEventToList(event: TechEvent) { const list = document.querySelector('#event-list') const element = getTeaserListElement(event) list.append(element) } ``` And here's the problem: We have to be very sure that an element with the ID `event-list` exists in our HTML. Otherwise `document.querySelector` returns `null`, and appending the list will break the application. ### Strict null checks With `null` being part of all types, the code before is both valid but also highly toxic. A simple change in our markup and the whole application breaks. We would need a way to make sure that the result of `document.querySelector` is actually available, and not null. Of course, we can do null checks or use the fancy elvis operator (also known as optional chaining), but wouldn't it be great if TypeScript tells us actively that we should do so? There is a way. In your `tsconfig.json` we can activeate the option `strictNullChecks` (which is part of strict mode). Once we activate this option, all nullish values are excluded from our types. ![The type number with strict null checks](https://i.imgur.com/ABRma1F.png) With `null` and `undefined` not being part of the actual type set, this piece of code will error during compile time: ```typescript let age: number age = age + 1 ^^^ ``` `age` is not defined, after all! But `strictNullChecks` does not change the way how `document.querySelector` works. The result can still be null. But the return type of `document.querySelector`. It's `Element | null`, a union type with the nullish value! And this makes TypeScript immediately throwing a red squiggly to us: ```typescript function appendEventToList(event: TechEvent) { const list = document.querySelector('#event-list') const element = getTeaserListElement(event) list.append(element) ^^^^ } ``` `list` is probably `null`. How right TypeScript is. A quick null check (the elvis operator dancing in front of us) does the trick and makes our code a lot safer: ```typescript function appendEventToList(event: TechEvent) { const list = document.querySelector('#event-list') const element = getTeaserListElement(event) list?.append(element) // 🕺 } ``` Typescript goes even a little bit further. With `strictNullChecks` enabled, we not only have to check for nullish values, we are also not allowed to assign `undefined` or `null` to variables and properties. Both values are removed from all types, so an assignment of that kind is forbidden. There are situations where we need to work with either `undefined` or `null`. To bring one (or both) values again back to the mix, we have to add them to a union, e.g. `string | undefined`. This makes adding nullish values explicit, and we have to explicitly check for their existence. ```typescript type Talk = { title: string, speaker: string, abstract: string | undefined } ``` Another way to add `undefined` is to make properties of an object optional. Optional properties have to be checked for as well, but without us maintaining too many types. ```typescript type Talk = { title: string, speaker: string, abstract?: string } ``` In any case, just like Doughlas Crockford said, why should we need two nullish values? If you need to use one, stick with one of them. ## Recap This chapter was all about type hierarchies, set theory, top and bottom types, and nullish values that can break our programs. Everything we learned in the scope of union and intersection types is crucial to everything that's coming up. Once you learn how to move around in the type space, TypeScript has to offer so much to you. 1. We learned about union and intersection types, and how we can model data that can take different shapes. 2. We also learned how union and intersection types work within the type space. We also learned about discriminating unions and value types. 3. We learned about const context, and found ways to dynamically create other types through lookup and mapped types. 4. We built our own type predicates as custom type guards. 5. The bottom type never is great for exhaustive checks within switch or if-else statements. 6. Last, but not least, we dealt with null and undefined and got pretty much rid of them. 7. One thing that is now native to us is widening and narrowing types. We can go from the all encompassing `any` down to the type with no values `never`. We can freely move around in the type space for all types that we know of. Now, let's learn what to do with types where we don't know how they're shaped. ## Interlude: Tuple types We got around the whole type spectrum of primitive types and object types, but there's one detail that we've left out: Arrays, and their sub-types. Consider this function signature: ```typescript declare function useToggleState(id: number): { state: boolean, updateState: () => void }; ``` You might see something like this when you use a library like React. It takes one parameter, a number. The name suggests it's an identifier, and it returns an object with the state of our toggle button, and a function to update this state. When we use this function, we want to use destructuring to have an easy access to its properties: ```typescript const { state, updateState } = useToggleState(1) ``` But what happens if we want to use more than one toggle state at the same time? ```typescript const { state, updateState } = useToggleState(1) // those variables are already declared! const { state, updateState } = useToggleState(2) ^^^^^^^^^^^^^^^^^^ ``` Object destructuring let's us go directly to the properties of an object, declaring them as variables. We can use array destructuring where we go directly to the indicies of an array, declaring them as variables under an entirely new name: ```typescript const [ first, updateFirst ] = useToggleState(1) const [ second, updateSecond ] = useToggleState(2) ``` Now we can use `first`, `second` and their state update methods freely in our code. Of course, we would require `useToggleState` to return an array instead. But how do we type this? We are dealing with two different types. One is a boolean, the other one a function with no parameters and no return value. This is not your average array with an technically endless amount of values of one type. Because it isn't. It's a tuple. While an array is a list of values that can be of any length, we know exactly how many values we get in a tuple. Usually, we also know the type of each element in a tuple. In TypeScript, we can define tuples. A tuple type for the example above would be ```typescript declare function useToggleState(id: number): [boolean, () => void] ``` Note that we don't define properties, just types. The order in which the types appear is important. Tuple type are sub-types of arrays, but they can't be infered. If we use type inference directly on a tuple, we will get the wider array type: ```typescript // tuple is `(string | number)[]` let tuple = ['Stefan', 38] ``` As with any other value type, declaring a const context can infer the types correctly: ```typescript // tuple is readonly [string, number] let tuple = ['Stefan', 38] as const ``` But this makes tuple also read-only, so be aware. As with any other sub-types, if we declare a narrower type in a function signature or in a type annotation, TypeScript will check against the narrower type instead of the wider, more general type: ```typescript function useToggleState(id: number): [boolean, () => void] { let state = false // .. some magic // type checks! return [false, () => { state = !state}] } ``` Without the return type, TypeScript would assume that we get an array of mixed boolean and function values.