# Chapter 5: Generics In the last chapter we learned how to move in the type space, and how we can narrow and widen sets of types. We get more type safe by knowing exactly what we can expect from our software at a particular point in time. But there are situations where we can't say for sure what awaits us. Situations where we have a notion of what's coming up, but some details are still shrouded in mystery. Still, we want to get type safety and all the nice tooling features TypeScript provides. With generics, we have a way to prepare for the unknown. We define types that describe a certain piece of the type system, where the details are filled out later. This is the land where utility functions and utility types are born. To illustrate the following chapter, think of a video player portal that features video streams in different qualities, subtitles in different languages, and user-centric features. ## Lesson 29: I don't know what I want, but I know how to get it The infamous line by the Sex Pistols perfectly describes the Punk of the 1970s, toddlers during tantrums, and generics. Consider the following data structure for a video that exists in different formats: ```typescript type VideoFormatURLs = { format360p: URL, format480p: URL, format720p: URL, format1080p: URL } ``` With `URL` being the browser built-in class of URLs. We want to provide an API where developers can load a specific format (using declare statements for brevity): ```typescript declare const videos: VideoFormatURLs declare function loadFormat( format: string ): void ``` To make sure that the incoming `format` is a valid key in our data structure, we create a utlity function with a type predicate, just like we did in the previous chapter: ```typescript function isFormatVailable( obj: VideoFormatURLs, key: string ): key is keyof VideoFormatURLs { return key in obj } ``` We've been there. The function works as intended, and in our type space we can narrow down the set of all strings to just the keys of `VideoFormatURLs`: ```typescript if(isFormatAvailable(videos, format)) { // format is now "format360p" | "format480p" | // "format720p" | "format1080p" // and index accessing perfectly works: videos[format] } ``` Now, we have a similar situation for loading subtitles. This is our subtitle data structure: ```typescript type SubtitleURLs = { english: URL, german: URL, french: URL } ``` And this is the validation function to check if a certain key is available in our subtitles object: ```typescript function isSubtitleAvailable( obj: SubtitleURLs, key: string ): key is keyof SubtitleURLs { return key in obj } ``` Wait a minute. This is exactly the same function! In JavaScript, we wouldn't create two of them, as they serve exactly the same purpose, and even have exactly the same implementation! But we kind of need two implementations as we want to have type safety, don't we? Well, here's one rule to live by: If we do something in TypeScript that we wouldn't do like that in JavaScript, we should rethink if we are on the right track. TypeScript was designed to provide type safety for almost all JavaScript scenarios. Correctly typing a utility function is definitely one of them. ### Enter generics Let's get one step back and define a utility function how we would do it in JavaScript, without any types: ```typescript function isAvailable( obj, key ) { return key in obj } ``` Now, we want to prepare our function for unknown types, and still give the correct answer. This is where generics come in. Generic programming is by [definition](https://en.wikipedia.org/wiki/Generic_programming) > [...] a style of computer programming in which algorithms are written in terms of types to-be-specified-later that are then instantiated when needed for specific types provided as parameters. This definition includes already some crucial information: Instead of working with a specific type, we work with a parameter that is then substituted for a specific type. Type parameters are denoted within angle brackets at function heads or class declarations. Let's add one to our `isAvailable` function. ```typescript function isAvailable<Formats>( obj, key ) { return key in obj } ``` The type `Formats` does not exist in our type declarations, but is a parameter that gets substituted for a real one, like `VideoFormatURLs` or `SubtitleURLs`. However, we can use this type parameter as we are used to with regular types within our function declaration: ```typescript function isAvailable<Formats>( obj: Formats, key ): key is keyof Formats { return key in obj } ``` If we want to type key -- which is now implicitly *any* -- we would need to use a wider set of possible key types: ```typescript function isAvailable<Formats>( obj: Formats, key: string | number | symbol ): key is keyof Formats { return key in obj } ``` This is because our generic type parameter `Formats` doesn't know that it can only have string keys, it has to prepare itself for all possible keys. In JavaScript, numbers and symbols are all valid key types. Take an array for instance, arrays can be seen as objects with number keys. ### Generic annotations and generic inference Now that we defined our function as a generic function, let's make use of it. We have two different ways of using the generic function. First, we can explicitly annotate the type we want to substitute: ```typescript if(isFormatAvailable<VideoFormatURLs>(videos, format)) { // ... } ``` Just like with explicit type annotations elsewhere, TypeScript takes this as a given and validates everything else against this type. This means that the moment we specify `VideoFormatURLs` to be the substitute for the type parameter, we have to make sure that the argument `obj` that we pass to the function matches the type `VideoFormatURLs`. However, it's much more interesting, and powerful, when we use type inference for substitute our type parameter. TypeScript is capable of inferring the type parameter from actual arguments you pass to a function, which feels much more natural: ```typescript // An object with video formats declare const videoFormats: VideoFormatURLs if(isAvailable(videoFormats, format)) { // infered type `VideoFormatURLs` // format is now keyof VideoFormatURLs } // An object with video formats declare const subtitles: SubtitleURLs if(isAvailable(subtitles, language)) { // infered type `SubtitleURLs` // language is now keyof SubtitleURls } ``` This is just writing JavaScript. ### Generics in the wild This was our first, self-written, generic function. You might have encountered some generics already. `Promise` is a generic type that pops up the moment you write asynchronous code. The argument in `Promise` gives you the result type: ```typescript // randomNumber returns a Promise<number> async function randomNumber() { return Math.random() } ``` Another one is `Array`. We can write array types with an array literal: ```typescript let anArray: number[] ``` Or we can use the generic ```typescript let anotherArray: Array<number> ``` Both do the same thing. You might however find it a bit more convenient to use the generic type when you deal with union types: ```typescript let aMixedArray: Array<number | string | boolean> ``` Especially when you work with lots of JavaScript's built-in APIs or browser APIs, you will encounter generics very soon. ## Lesson 30: Generic constraints Our generic function is already pretty good. We can pass anything from the wide variety of types available and can pinpoint concrete types once we substitute. When we think of sets, we open up a type for *any*, and then once we substitute, we select a much narrower set. This can lead to some undesired behaviour, however. Our `isAvailable` type from the last lesson works really well with the object types we defined: ```typescript function isAvailable<FormatList>( obj: FormatList, key: string | number | symbol ): key is keyof FormatList { return key in obj } // An object with video formats declare const videoFormats: VideoFormatURLs if(isAvailable(videoFormats, format)) { // infered type `VideoFormatURLs` // format is now keyof VideoFormatURLs } // An object with video formats declare const subtitles: SubtitleURLs if(isAvailable(subtitles, language)) { // infered type `SubtitleURLs` // language is now keyof SubtitleURls } ``` And also with all other object types that are available, even ones without a concrete type declaration: ```typescript if(isAvailable({ name: Stefan, age: 38}, key)) { // key is now "name" | "age" } ``` But it also works with all non-object types: ```typescript if(isAvailable('A string', 'length')) { // also strings have methods, // like length, indexOf, ... } if(isAvailable(1337, aKey)) { // alos numbers have methods // aKey is now everything Number has to offer } ``` It also works with arrays, resulting that the key can be the entire set of numbers as well as array functions like `map`, `forEach`, etc. While this is cool, as it makes our types even more compatible, it can lead to undesired behavour if we only want to check objects. Thankfully, TypeScript has a way to deal with situations like this. ### Defining boundaries As we explained initially, type parameters of generics cover the entire set of types, so *any*. Only by substituting with a specific type, the type set gets narrower and clearer. However, there's the possibility to define boundaries, or subsets of the type space. This makes generic type parameters a little bit narrower before being substituted by real types. We as developers get information upfront if we pass an object that shouldn't be passed. To define generic subsets, TypeScript uses the *extends* keyword. We check if a generic type parameter *extends* a specific subset of tpyes. If we only want to pass objects, we can extend from the type object: ```typescript function isAvailable<FormatList extends object>( obj: FormatList, key: string ): key is keyof FormatList { return key in obj } ``` With `<FormatList extends object>`, we tell TypeScript that the argument we pass needs to be at least an object. All primitive types and even arrays are excluded. ```typescript isAvailable('A string', 'length') ^^^^^^^^^^ ``` Red squigglies where they are supposed to be. ### Index types Let's define a function that loads a file, either a video in a specific format, or a subtitle in a specific language. Again, we start with the raw JavaScript function (just the head for brevity): ```typescript function loadFile(fileFormats, format) { // implement 🤓 } ``` When we add types, we would do somethign similar as with the `isAvailable` function: ```typescript function loadFile<Formats extends object>( fileFormats: Formats, format: string ) { // you know 🤓 } ``` We can even go further. When we look at both our format definitions, you can recognize another common feature. ```typescript type VideoFormatURLs = { format360p: URL, format480p: URL, format720p: URL, format1080p: URL } type SubtitleURLs = { english: URL, german: URL, french: URL } ``` That's right, all properties are of type `URL`. Another format would most likely cause an error when used with the `loadFiles` function. We would need a constraint to ensure that we only pass compatible objects, where we don't know the properties themselves, we only know that every property is of type `URL`. Index types, like we have briefly seen in the last chapter, are perfect for that. This is an index type that we met before, iterating over a set of unions, and in this case allowing any value for each property ```typescript type PossibleKeys = 'meetup' | 'conference' 'hackathon' | 'webinar' type Groups = { [k in PossibleKeys]: any } ``` Index types don't define specific property keys, they just define a set of keys they iterate over. We can also accept the entire set of strings as keys. ```typescript type AnyObject = { [k: string]: any } ``` Now that we accept all property keys of type string, we can explicitly say that the type of each property needs to be `URL`: ```typescript type URLList = { [k: string]: URL } ``` A perfect shape that includes `VideoFormatURLs` as well as `SubtitleURLs`. And basically any other list of URLs! Therefore, also a perfect constraint for our generic type parameter: ```typescript type URLList = { [k: string]: URL } function loadFile<Formats extends URLList>( fileFormats: Formats, format: string ) { // the real work ahead 🤓 } ``` With that, every object that we pass that doesn't give an object with URLs is going to create beautiful, red, squiggly lines in our editor. ## Lesson 31: Working with keys We defined an object that allows for any key of type string, as long as the type of each property is a `URL`. With that, we already know that we only can pass objects that have the correct shape without the compiler complaining. However, when we select the right format, we still can pass every string to the function, even though the format might not exist. ```typescript declare const videos: VideoFormatURLs loadFile(videos, 'format4k') // 4K not available 😕 // TypeScript doesn't squiggle 😕 ``` Of course, we can do better. ### Related type parameters We only want to allow to pass keys as the second argument that are actually available in the object. In a non-generic world, we would do something like this: ```typescript function loadVideoFormat( fileFormats: VideoFormatURLs, format: keyof VideoFormatURLs ) { // 🤓 you know } ``` And the same applies to generic type parameters: ```typescript type URLObject = { [k: string]: URL } function loadFile<Formats extends URLObject>( fileFormats: Formats, format: keyof Formats ) { // the real work ahead 🤓 } ``` This already gives us great tooling. We now only can enter keys which are part of the object we pass as the first parameter: ```typescript loadFile(video, 'format1080p') // 👍 // 'format4k' is not available loadFile(video, 'format4k') ^^^^^^^^^^ ``` `keyof Formats`, when substituted with `VideoFormatURLs` yields `"format360p" | "format480p" | "format720p" | "format1080p"`. The format we pass for the second argument needs to be within this union type. Let's take a look at the function body, and let us do a very straightforward implementation. We access the URL, fetch some data from it, and return an object that tells us what format we loaded, and if loading was successful. An actual implementation would have much more detail, but this is all we need to see what's happening on a type level. As we are using the async `fetch` function, we are transforming `loadFile` to be an async function as well. ```typescript async function loadFile<Formats extends URLObject>( fileFormats: Formats, format: keyof Formats ) { // fetch the data const data = await fetch(fileFormats[format].href) return { // return the format format, // and see if we get an OK response loaded: data.response === 200 } } ``` Let's see what we get in return. Thanks to type inference, the return type of `loadFile` is `Promise<{ format: keyof Formats, loaded: boolean }>`. Promise is a generic type, which shouldn't come to a surprise by now. And the property `format` in our return value is the generic type parameter we defined in our function. Let's use our function with substitutes. ```typescript const result = await loadFile(videos, "format1080p") ``` `await` is unwrapping the `Promise<>`, so we can see at the actual return values from `loadFile`. `result` is of type `{ format: "format360p" | "format480p" | "format720p" | "format1080p", loading: boolean }`. As we expect, we get `keyof VideoFormatURLs` as return. But shouldn't we know more? We are explicitly passing `"format1080p"` as second argument. We've narrowed down the union through usage already to a single value type. Why can't `result` be of type `{ format: "format1080p", loading: boolean }`? We can achieve this by adding a second type parameter to our generic declaration. One that shows the relation to the first, but works as its own type once we declared it, like this: ```typescript function loadFile< Formats extends URLObject, Key extends keyof Formats >(fileFormats: Formats, format: Key) { const data = await fetch(fileFormats[format].href) return { format, loaded: data.response === 200 } } ``` The second type parameter `Key` is subtype of `keyof Formats`, the first type parameter. The interesting part now happens when we start subtituting: ```typescript loadFile(video, 'format1080p') // 👍 ``` `video` is of type `VideoFormatURLs`. `VideoFormatURLs` is a subtype of `URLObject`, so the type-check passes and `Formats` can be substituted. Now `Key` needs to be a subtype of `keyof Formats`. `"format1080p"` is a subtype of `keyof Formats`, so the type-check passes, and `Key` can be substitued. Now we've locked in and substitued two types: 1. `Formats` is `VideoFormatURLs` 2. `Key` is `"format1080p"` That's right. Now that we've passed a literal string, the type parameter takes the literal, the value type. Which means that once we execute this function and look at the result, we can be entirely sure that `result.format` is `"format1080p"`: ```typescript const result = await loadFile(videos, "format1080p") if(result.format !== "format1080p") { // result.format is now never! throw new Error("Your implementation is wrong") } ``` To make sure that we're also implemented the right thing, we define a return type for the `loadFile` function where we expect the `Key` type to appear. ```typescript type URLObject = { [k: string]: URL } type Loaded<Key> = { format: Key, loaded: boolean } async function loadFile< Formats extends URLObject, Key extends keyof Formats >(fileFormats: Formats, format: Key): Promise<Loaded<Key>> { const data = await fetch(fileFormats[format].href) return { format, loaded: data.response === 200 } } ``` All wrapped in a generic `Promise` as we are async. ## Lesson 32: Generic mapped types TypeScript has a couple of helper types that can be used for what we did manually. They might come in handy when we start creating advanced types. Let's look and `Record` and `Pick`. Both are *mapped types* with *generics*. ### Pick There's `Pick<O, K>`, where you create a new object with selected property keys `K` of object `O`. It is defined as ```typescript type Pick< O, K extends keyof O > = { [P in K]: O[P]; } ``` `[P in K]` runs over all value types in the union K, which is all keys of `O`. `O[P]` is an *indexed access type*. It's like indexing an object, but retreiving a type. This allows us to define a union of keys that are part of an original object type, and select those keys, and their types from the original object. For example, this would be a type with all HD videos ```typescript type HD = Pick< VideoFormatURLs, 'format1080p' | 'format720p' > // equivalent to type HD = { format1080p: URL, format720p: URL } ``` The `Pick` helper type's most obvious use is the `pick` utility function that you get from libraries like `lodash`. But, it can be helpful in other scenarios as well. We're getting there in the later chapters. ### Record `Record<K, T>` creates an object type where all types in `T` get the type `K`. Like a dictionary. It is defined as ```typescript type Record< K extends string | number | symbol, T > = { [P in K]: T } ``` Note that `K` is a subtype of `string | number | symbol`. We met this trio earlier on, as they are the allowed types for object keys. `URLObject` from the previous lesson would be defined as ```typescript type URLObject = Record<string, URL> ``` `Record` is a neat shorthand if we need to create an object type on the fly. ### Mapped and indexed access types Let's say that our video platform, while allowing for all four kinds of video resolutions to be uploaded, doesn't require all four of them. We require at least one format. Modeling this situation is easily done with union types: ```typescript type Format360 = { format360p: URL } type Format480 = { format480p: URL } type Format720 = { format720p: URL } type Format1080 = { format1080p: URL } type AvailableFormats = Format360 | Format480 | Format720 | Format1080 const hq: AvailableFormats = { format720p: new URL('...'), format1080p: new URL('...') } // 👍 const lofi: AvailableFormats = { format360p: new URL('...'), format480p: new URL('...') } // 👍 ``` With union types, we only need to fulfil the contract of one union constituent. This makes it great if we need minimum one random property set, and all others optional. But -- you guessed it -- it would require us to maintain a second set of types. We don't want to re-define `VideoFormatURLs` as the type is necessary for certain functionality in our app. We just want to have `VideoFormatURLs`, but split into unions. Let's build a helper, called `Split`. The goal is to create a union type. To make it easier, we start with a concrete type, and work with the substitution later. So what do we already know? First. We know `keyof VideoFormatURLs` creates a union of all keys of `VideoFormatURLs`. ```typescript type Split = keyof VideoFormatURLs // equivalent to type Split = "format360p" | "format480p" | "format720p" | "format1080p" ``` We also know that a *mapped* type runs over all keys and creates a new object with those keys. The following example creates the same type as `VideoFormatURLs`, but with the key being also the value: ```typescript type Split = { [P in keyof VideoFormatURLs]: P } // equivalent to type Split = { format360p: "format360p", format480p: "format480p", format720p: "format720p", format1080p: "format1080p" } ``` Now, we can acces the *values* of this type again by using the *indexed access operator*. If we access by the union of keys of `VideoFormatURLs`, we get a union of the values. ```typescript type Split = { [P in keyof VideoFormatURLs]: P }[keyof VideoFormatURLs] // equivalent to type Split = "format360p" | "format480p" | "format720p" | "format1080p" ``` This looks exactly like the first step, but it's fundamentally different. Instead of getting the left side of an object type -- the property keys -- in union, we get the right side of an object type -- the property types -- in union. So the only thing we have to do is to get the values right, and we have the union we invisioned. Enter Record. A `Record<P, VideoFormatURLs[P]` gives us an object with the property P we get from the key union, and we're accessing the corresponding type from from the property key. ```typescript type Split = { [P in keyof VideoFormatURLs] : Record<P, VideoFormatURLs[P]> }[keyof VideoFormatURLs] // equivalent to type Split = Record<"format360p", URL> | Record<"format480p", URL> | Record<"format720p", URL> | Record<"format1080p", URL> // equivalent to type Split = { format360p: URL } | { format480p: URL } | { format720p: URL } | { format1080p: URL } ``` Last, but not least, let's build a generic out of it. ```typescript type Split<Obj> = { [Prop in keyof Obj]: Record<Prop, Obj[P]> }[keyof Obj] type AvailableFormats = Split<VideoFormatURLs> ``` The moment we change something in `VideoFormatURLs`, we are updating `AvailableFormats` as well. And TypeScript yells at us with wonderful red squigglies if we have set a property that doesn't exist anymore. ## Lesson 33: Mapped type modifiers Our video application allows for signed-in users. Once a user is signed-in, they can define preferences on how they want to consume their video content. A simple type modeling user preferences can look like this: ```typescript type UserPreferences = { format: keyof VideoFormatURLs subtitles: { active: boolean, language: keyof SubtitleURLs }, theme: 'dark' | 'light' } ``` The references to `VideoFormatURLs` and `SubtitlteURLs` make sure we don't have to maintain more types than necessary. Updating one of these types adds another part to the union of keys at `format` and `subtitles.language`. Also, instead of allowing every string to be a valid `theme`, we restrict this property to be either `dark` or `light`. ### Partials As you can read from the type `UserPreferences`, no property is optional. All properties are required to produce a sound user experience, we don't want to leave anything out. To ensure all keys are set, we provide a set of default user preferences: ```typescript const defaultUP: UserPreferences = { format: 'format1080p', subtitles: { active: false, language: 'english' }, theme: 'light' } ``` We use a type annotation here. Usually we try to infer as much as possible, but defaults fall into the category of maintained objects. `defaultUP` can change, and the moment we change it, we want to validate it against `UserPreferences`. For our users, we just store deltas. If a user selects their preferable video format to something different, it's just this new format that we are storing. ```typescript const userPreferences = { format: 'format720p' } ``` To get to the full set of preferences, we are mering our default preferences with the user's preferences in a function: ```typescript function combinePreferences(defaultP, userP) { return { ...defaultP, ...userP } } ``` Using the object spread syntax, we create an object that is a copy of `defaultP`, and override or extend with all properties from `userP`. The resulting object is the full user preferences, with the delta applied. Now, let's add types to this function. `defaultP` is easy to type: ```typescript function combinePreferences( defaultP: UserPreferences, userP: unknown ) { return { ...defaultP, ...userP } } ``` But how do we type `userP`? We would need a type where every key can be optional, something like this: ```typescript type OptionalUserPreferences = { format?: keyof VideoFormatURLs subtitles?: { active?: boolean, language?: keyof SubtitleURLs }, theme?: 'dark' | 'light' } ``` But of course, we don't want to maintain that type ourselves. Let's create a helper type `Optional` that takes for us. This is a mapped type, where we modify the property features so each key becomes optional: ```typescript type Optional<Obj> = { [Key in keyof Obj]?: Obj[Key] } ``` Note the little question mark next to the mapped argument where we iterate through all the keys. This is called a property modifier. With that, we create a copy the type parameter `Obj` where all keys are optional. Let's annotate our function `combinePreferences` with this helper type. ```typescript function combinePreferences( defaultP: UserPreferences, userP: Optional<UserPreferences> ) { return { ...defaultP, ...userP } } ``` Now, we get extra autocomplete and type safety when using `combinePreferences`. ```typescript // 👍 const prefs = combinePreferences( defaultUP, { format: 'format720p' } ) // 💥 const prefs = combinePreferences( defaultUP, { format: 'format720p' } ^^^^^^^^^^^^^^^^^^^^^^ ) ``` `Optional<Obj>` is a built-in type in TypeScript called `Partial<Obj>`. It also has a reversed operation `Required<Obj>` which makes all keys required by removing the optional property modifier. It is defined as: ```typescript type Required<Obj> = { [Key in Obj]-?: Obj[Key] } ``` ### Readonly One thing we want to ensure is that `defaultUP` is not mutable from other parts of our software. It should be maintained in code, not by a side effect. On a tooling perspective, we need a type that ensures every property is a readonly property. ```typescript type Const<Obj> = { readonly [Key in Obj]: Obj[Key] } ``` You see that we add a propery modifier `readonly`. With that, `defaultUP` is not getting any updates without TypeScript complaining. ```typescript const defaultUP: Const<UserPreferences> = { format: 'format1080p', subtitles: { active: false, language: 'english' }, theme: 'light' } defaultUP.format = 'format720p' ^^^^^^ ``` `Const<Obj>` is available in TypeScript as `Readonly<Obj>`. In JavaScript we still would be allowed to modify that object. That's why we use `Object.freeze` to make sure we can't change anything at runtime. The return value's type of `Object.freeze` is `Readonly<Obj>`. ```typescript function genDefaults(obj: UserPreferences) { return Object.freeze(obj) } const defaultUP = genDefaults({ format: 'format1080p', subtitles: { active: false, language: 'english' }, theme: 'light' }) // defaultUP is Readonly<UserPreferences> defaultUP.format = 'format720p' ^^^^^^ ``` This errors in both TypeScript *and* JavaScript. ### Deep modifications There's one thing to keep in mind with `Readonly` and `Partial`. Our nested data structure. For example, this call will cause some errors in TypeScript: ```typescript const prefs = combinePreferences( defaultUP, { subtitles: { language: 'german' } } ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ) ``` TypeScript expects us to provide the full object for `subtitles`, as `Partial` just made the first level of properties optional. With the kind of assigment we are doing, this is actually expected behaviour. The call above would override our `subtitles` property and delete `subtitles.active`. We would need to do more sophisticated assignments, and also more sophisticated types. A similar problem pops up when we are looking at our default preferences. `Readonly` only modifies the first level of properties, which means that this call does not error in TypeScript, whereas it breaks once it runs in the browser: ```typescript defaultUP.subtitles.language = 'german' ``` To make sure our types are what we expect them to be, we need helper types that go deeper than one level. Thankfully, TypeScript allows for recursive types. We can define a type that references itself, and goes one level deeper. See `DeepReadonly` for instance: ```typescript type DeepReadonly<Obj> = { readonly [Key in Obj]: DeepReadonly<Obj[Key]> } ``` TypeScript knows to stop the recursion if `Obj[Key]` returns a primitive or value type, or a union of primitive or value types. Let's apply the new helper type to our `genDefaults` function as return type: ```typescript function genDefaults( obj: UserPreferences ): DeepreadOnly<UserPreferences> { return Object.freeze(obj) } ``` As `Readonly` is a subtype of `DeepReadonly`, the narrower return type of `Object.freeze` is compatible with the wider return type we defined. The same can be done for partials: ```typescript type DeepPartial<T> = { [P in keyof T]?: DeepPartial<T[P]> } ``` But the details of the new implementation are up to you! ## Lesson 34: Binding generics Let's revisit `combinePreferences` again. We spoke a lot about what arguments we want to pass into this function, that we haven't had a look on what's being returned by our operation. ```typescript function combinePreferences( defaultP: UserPreferences, userP: Partial<UserPreferences> ) { return { ...defaultP, ...userP } } const prefs = combinePreferences( defaultUP, { format: 'format720p' } ) ``` When we hover over `prefs`, we can see the outcome of what TypeScript infers from our assignment: ```typescript const prefs: { format: "format360p" | "format480p" | "format720p" | "format1080p"; subtitles: { active: boolean; language: "english" | "german" | "french"; }; theme: "dark" | "light"; } ``` This is the same as `UserPreferences`, and what we expected. With one argument being `UserPreferences`, and the other being `Partial<UserPreferences>`, the combination of both arguments should be the full `UserPreferences` type again. Getting `UserPreferences` in return from `combinePreferences` is perfectly fine behaviour and will make your app a lot more type safe than it was before. Let's take this as an opportunity to explore type annotations, type inference, and generic type binding, and see its effects. ### Type inference Our user's preferences are a video format of 720p and a dark theme. The corresponding object is: ```typescript { format: 'format720p', theme: 'dark' } ``` We use this literal as a literal argument for `combinePreferences`. ```typescript combinePreferences( defaultUP, { format: 'format720p', theme: 'dark' } ) ``` The moment we pass the literal, TypeScript infers the type of our literal to be the value type. This is because this value, being an argument of a function, can't change through operations. The only way we can modify this value is by editing the source code. It's final. When we assign this value to a variable, things are different. ```typescript const userSettings = { format: 'format720p', theme: 'dark' } combinePreferences( defaultUP, userSettings ^^^^^^^^^^^^ ) ``` The moment we assign this value to `userSettings`, TypeScript infers its type to the most reasonably widest type. In our case, strings. ```typescript // typeof userSettings = { format: string, theme: string } ``` This type is much wider from what we expect in our `UserPreferences` type. TypeScript will throw red squigglies to us because we can't take the wider `string` from `"dark" | "light"`, nor for all the formats we listed. And TypeScript is right! There is no security that we don't change the value at some point to something entirely incompatible. Thank you, TypeScript! One thing we could do is adding const context: ```typescript const userSettings = { format: 'format720p', theme: 'dark' } as const ``` Sealing it from change in TypeScript and narrowing the assingment down to its value type. Thus being compatible with `Partial<UserPreferences>` as it is a subtype. The other thing we could do is doing a type annotation. ### Type annotations While we advocate for using as much inference as reasonably possible, annotations are a magical thing that we can use in cases where our types are very narrow to not comply with primitive types. Type annotations do a type-check the moment we assign a value. ```typescript const userSettings: Partial<UserPreferences> = { format: 'format720p', theme: 'dark' } ``` With that type annotation, `userSettings` will always be `Partial<UserPreferences>` as long as the values we assign type-check. If they type-check, we will never get back to their original values when using the variable further on. This information is lost to us. ### Generic type binding We call the process of substituting a concrete type for a generic "binding". Let's inspect what happens if we bind a generic type parameter to a concrete type. This is `combinePreferences` with a generic type parameter. ```typescript function combinePreferences< UserPref extends Partial<UserPreferences> >( defaultP: UserPreferences, userP: UserPref ) { return { ...defaultP, ...userP } } const prefs = combinePreferences( defaultUP, { format: 'format720p', theme: 'dark' } ) ``` When we call `combinePreferences` with an annotated type `Partial<UserPreferences>`, we substitute `UserPref` for its supertype. We get the same behaviour as we had originally. When we call `combinePreferences` with a literal or a variable in const context, we bind the value type to `UserPref` 1. `{ format: 'format720p', theme: 'dark' }` is taken as literal, therefore we look at the value type. 2. The value type `{ format: 'format720p', theme: 'dark' }` is a subtype of `Partial<UserPreferences>`, so it type-checks. 3. We bind `UserPref` to `{ format: 'format720p', theme: 'dark' }`, which means that we now work with the value type, instead of `Partial<UserPreferences>` `UserPref` has changed now, this means that our result type has changed as well. If we hover over `prefs`, we get the following type information: ```typescript const p: { format: "format360p" | "format480p" | "format720p" | "format1080p"; subtitles: { active: boolean; language: "english" | "german" | "french"; }; theme: "light" | "dark"; } & { format: "format720p"; theme: "dark"; } ``` First, we learn what the operation `{...defaultP, ...userP}` actually does. It creates a combination of two objects, and the resulting type is an intersection. This makes sense! We also see what `UserPrefs` became the moment we passed a literal: the value type of said literal. This intersection creates an interesting behaviour. We have a couple of union types that are now intersected with subtypes of their sets. In such a scenario, the narrower set always wins: ```typescript ('dark' | 'light') & 'dark' // type is 'dark' ``` Which means that we know exactly which values we get when we work with `prefs`: ```typescript prefs.theme // is of type 'dark' prefs.format // is of type 'format720p' ``` This makes some checks in our code easier. Be careful though with too many value types. If we take the same pattern for the default preferences and pass a const context object to it, we might get some unwanted side effects: ```typescript function combinePreferences< Defaults extends UserPreferences, UserPref extends Partial<UserPreferences> >( defaultP: Defaults, userP: UserPref ) { return { ...defaultP, ...userP } } const defaultUP = { // we know what we have here } as const const prefs = combinePreferences( defaultUP, { format: 'format720p', theme: 'dark' } ) ``` The resulting type looks like this: ```typescript const prefs: { readonly format: "format1080p"; readonly subtitles: { readonly active: false; readonly language: "english"; }; readonly theme: "light"; } & { format: "format720p"; theme: "dark"; } ``` The intersection of two distinct value types always results in `never`. Which means that both `theme` and `format` become unusable to us. ## Lesson 35: Generic type defaults In the last lesson of this chapter we want to show videos inside a video element. To make it easier for us and our co-workers, we decide to abstract handling with the DOM, and we choose to use classes for this. The class should behave like this: 1. We can instantiate as many as we like and pass our user preferences to it. The user preferences are important to select the right video format URL. 2. We can attach any HTML element to it. If it's a video element, we load the video source directly. If it's any other element, we use it as a wrapper for a newly created video element. Video elements are the default, though. 3. The element is not required for instantiation, we can set it at a later stage. This means the element can be undefined the moment we load a video. Let's implement this class. ### Moving to generics First, we create a helper type `Nullable` that adds `undefined` in a union. This makes reading field types of classes much more readable. ```typescript type Nullable<G> = G | undefined ``` Next, we start with the class. We set the type of the element to `HTMLElement` as this is the supertype of all html elements. ```typescript class Container { #element: Nullable<HTMLElement>; #prefs: UserPreferences // we only require the user preferences // to be set at instantiation constructor(prefs: UserPreferences) { this.#prefs = prefs } // we can set the element to an HTML element set element(value: Nullable<HTMLElement>) { this.#element = value } get element(): Nullable<HTMLElement> { return this.#element } // we load the video inside a video element // if #element isn't an HTMLVideoElement, we // create one and append it to #element loadVideo(formats: VideoFormatURLs) { const selectedFormat = formats[this.#prefs.format].href if(this.#element instanceof HTMLVideoElement) { this.#element.src = selectedFormat } else if(this.#element) { const vid = document.createElement('video') this.#element.appendChild(vid) vid.src = selectedFormat } } } ``` And this already works wonderfully: ```typescript const container = new Container(userPrefs) container.element = document.createElement('video') container.loadVideo(videos) ``` `HTMLElement` can be way too generic for some tastes. Especially when we deal with videos, we might want to work with the video functions of `HTMLVideoElement`. And when working with that, we sure want to have the right type information. Generics can help with that. We can pinpoint the exact type we are dealing with, and with type constraints we can make sure it's an extension of our supertype `HTMLElement`. ```typescript class Container<GElement extends HTMLElement> { #element: Nullable<GElement>; // ...abridged... set element(value: Nullable<GElement>) { this.#element = value } get element(): Nullable<GElement> { return this.#element } // ...abridged... } ``` This is better, but we are not entirely happy with it. ### Adding defaults As we lack a concrete element in the constructor, TypeScript has nothing to infer to bind `GElement` to a concrete type. We fall back to the supertype, `HTMLElement` without an explicit generic annotation: ```typescript // container accepts any HTML element const container = new Container(userPrefs) // container accepts HTMLVideoElement const vidcontainer = new Container<HTMLVideoElement>(userPrefs) ``` And this is bad, as our default should always be `HTMLVideoElement`. Other elements are the exception. This is where generic default parameters come in. If we don't provide a generic annotation, TypeScript will use the default parameter as type. ```typescript class Container< GElement extends HTMLElement = HTMLVideoElement> { // ... } // container accepts HTMLVideoElement const container = new Container(userPrefs) ``` Compared to type constraints, generic default parameters don't create a boundary, but a default value in case we can't infer or don't annotate. If a generic default parameter exists without a boundary, the generic can accept *any*. Like function default parameters, generic default parameters have to come last in a generic definition. Generic default parameters are extremely useful for classes that need to bind a generic, but don't have the information at instantiation. For all other cases, type constraints work best. ### Generic default parameters and type inference Whyle generic default parameters can be extremly powerful, we also have to be very cautious. Take this function that does something similar like the `Container` class. It loads a video in an element, and differentiates between the following cases: 1. If we don't provide an element, we create a video element 2. If we provide a video element, we load the video in this element 3. If we provide any other element, we use this as a wrapper for a new video element. The function returns the element we passed as an argument for further operations. With generic default parameters we can beautifully define this behaviour, and rely only on type inference: ```typescript declare function createVid< GElement extends HTMLElement = HTMLVideoElement >( prefs: UserPreferences, formats: VideoFormatURLs, element?: GElement ) ``` If we try it out, we get the following: ```typescript declare const userPrefs: UserPreferences declare const formats: VideoFormatURLs // a is HTMLVideoElement, the default! const a = createVid(userPrefs, formats) // b is HTMLDivElement const b = createVid( userPrefs, formats, document.createElement('div')) // c is HTMLVideoElement const c = createVid( userPrefs, formats, document.createElement('video')) ``` However, this only works when we rely solely on type inference. Generics also allow to bind the type explicitly. ```typescript const a = createVid<HTMLAudioElement>(userPrefs, formats) ``` `a` is of type `HTMLAudioElement`, even tough our implementation will return an `HTMLVideoElement`. Also, since we are on a type level, the implementation has no clue that we want to have an `HTMLAudioElement`. That's why we need to be cautious when we use generic default parameters. Also, we have a much better tool for cases like that, as we are going to see in the next chapter. ## Recap Generics allow us to prepare for types that we don't know upfront. This allows us to design robust APIs with better type information, and make sure that we only pass values where our types match certain criteria. 1. With generics we made sure that we don't have to create more functions just to please the type system. Generics allow us to generalize functions for broader usage. 2. Generic constraints allow us to create boundaries. Instead of accepting *anything* for our generic types, we are allowed to set some criteria, e.g. the existence of certain keys or types of properties. 3. Generics also allow us to work better with object keys. Depending on what we pass as argument to a function, we can infer the right keys and let TypeScript throw red squigglies at us if we don't provide the correct arguments. 4. Generics work extraordinarily well with mapped types. Through maps of union keys, index access types and the `Record` helper, we are e.g. able to create a type that allows us to split an object type into a set of unions. 5. Mapped type modifiers allow us to copy an object types, but set all properties optional, required, or readonly. 6. We learned a lot about binding generics. The moment we substitute a generic type for a real one is crucial to understand the TypeScript type system. 7. We also saw how generic classes work, and how we use generic type defaults to make our life a little bit easier. Working with generics is key to get the most out of TypeScript's type system. It was designed to comfort the majority of real-world JavaScript scenarios, and opens doors to even better, and more robust type information. We will spend the next chapter to go even further! ## Interlude: On names Generics is not something entirely new to programming. They have been around for a long time, with one of the first programming languages being Ada, which introduced a generic concept in the late 70s. Syntax-wise, generics as we use them today in TypeScript are a descendant of C++ templates. This comes to no surprise, as Java, JavaScript, C#, and many other languages are heavily inspired by the way the C/C++ described its programs. For generics, TypeScript borrows the angle brackets syntax. Even tough C++ templates are much more powerful than type substitution, the syntax has lead to name generic type parameters mostly `T`, short for template. Subsequent parameters usually go either up the alphabet (U, V, W) or are P for property, K for key, and so on. This can lead to highly unreadable types. If I throw `Record<T, U>` on you, with no understanding of what `Record` does, you might wonder what we should expect from the types we pass along. A `Record<Obj, PropType>` might be clearer: We can pass objects, and types for properties. So, even though it is common to use single letter generics, I advise you to do better. Types should be documentation, and that's why we have to be as explicit as possible with generic type parameters. This is my style-guide: 1. Uppercase words, no single letters. Uppercase to differentiate it from function parameters. 2. Highly abbreviated, but still readable. `Obj` is clearer than `O`, shorter than `Object`. `URLObj` indicates it is an object with URL properties. 3. Using prefixes to differentiate from actual types. E.g. the type `Element` exists in the DOM API. I use `GElement` for my generic type parameter (`Elem` is also an option). 4. `G` is a prefix for `generic`, but we can be clearer if we e.g. handle keys. `URLObject` is an object with URLs, so `UKey` is a key from this object. It's not a lot, but it makes generics a lot more readable.