# 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.