WEIRD TS TYPES 🛸: Using Contextual Typing and Deferred Inference to Plan an Alien Conquest
Tags: typescript, tutorial
Published: 2021-12-01 23:42:21

_This post is accompanied by a Typescript playground found [here](https://www.typescriptlang.org/play?#code/C4TwDgpgBAShDOECGAnAxgCwCqoOYWCgF4oBvAKCigG0UFl0MBlAVwCMArCNYALinjAUASwB2uALr8AgihRIQAHkEjxAPgDc5AL5byoSFAAKEFPAD2o4lABEgPg3Ai7s2tB6AGFzAd2s3AvBuA6Hed9cHdzFFEAMWEIABsAEx9AHg3AXn2UlJsoAB9bFMAB4nS9V1gWeRisLysSCipIczAYiBk5BUUTM0tNSig0L3gm+SUPT06qHvCo2Li+qFkBxQ9x6PjO3XJyHtFBAQBbJBiYgDdhfcbi0vLPSrIu2vrT6nsnABpbRxsJJ66ez2mHgJsXn5AoD-h8vmFIkspvwHmlUskATl4ekwatgoYAKIAD0gcWEwGEljcGCQ4ggEVEiiwUAgWOAEFEU1g9FQmBwKHwwDU1gAFMAKvwsABKYjcrB6NAxJDweDMxCs5gYYRgKk0ukMplweWMdmc7nVKBgdgxYRobqWOIsHjY3H4wmbHkAOmdtNtBMs01mLRtEDx7tExNJ+ApVLUaiF-ANVEs-D5AqgwtFCa0VG01yoVDowBKVijGZjUDjlxFRDUXQzGddvrtHsddEtaAgPJ5SAAZvSUD6-fb4C8q93LC9hCX9eWKxmNls2x2uzWrv25y326ZZ-74EKU+OK5PzA1HTFzLgeQADaTLlBqt32qAAElIwigAGooABGbS8Y8vacrnHV-0bsctyzHMoG-TtfwHURNy3bQXn5YtPgrVZUx0NZJ0IeBdn2C5RC1BhMCYJUwGsUQIG8PCFUI5VFBgEo9hwtQeQA9FoAAOXMABJURW1MUNrGpWl6UZWUxB4i8mCgAB+KAJP4UiDlMPRWxYUQeGvMQOzEiBVUEjVZQonU8AIRiujArjNNMBlG34IxUAJPZFHYrixNDMsIygVd7UDMkQywUdMwIECi1EEd0wzYDwjCitnUdeDREQ8cYrM0QLLoVSIAS1NN1WbQ1mU1T-RpeQ8TQJB6R09VhLlfDsCMrkmP4TyiRJHzKT8sKIqsYLQrzTqoAAeU4bhgEdaV4GEXBRB5QCyFgmaYsGrgeEdABrCAQHgbrHV2MBm3gdgluAUKeVIGh9qGngpBoCQoG0IUhTHACstQ9YPV3CB90PHkbAAIQgVswmgBc114RFML2Q5jgaZj0J2CGjhOM8ZwgudZRIcHsIqAyCKIrpHQ2BtgCah0xw00wtJO81FkmX4bDhdJATyFFbqFTKoDJlAKdO75fhuu62Y5rnDQgOoGhhR53hZtnTCQEqyqbB6qEVqBHUsHkMchk4AKAA)_

Typescript has a powerful inference system which automatically assigns types to variables and function parameters when they are not explicitly provided. The behavior of inference is [thoroughly documented in the Typescript Handbook](https://www.typescriptlang.org/docs/handbook/type-inference.html).

Inference is helpful, but there are some cases (like writing generic functions) where the order of inference needs to be controlled.

This was recently exemplified by [an RFC in the state library NGXS](https://github.com/ngxs/store/discussions/1805). In short, one of the functions published by the library was improperly typed because the type of its first argument was intended to be inferred from its contextually expected return type. 

Instead of explaining the RFC directly, however, I wanted to have some fun and explain it through ALIENS instead 👽.

Let's look at how contextually inferred, deferred types can help a ~~state management~~ alien invasion library...

---

### Invasion Overview

Let's say our alien overlords want us to plan an invasion of Smallville, a rural town in the US.

Smallville is like many small towns, so we'll create types that describe it in general terms.

```typescript
type Person = '🧑';
type Cow = '🐮';
// regular field or field with a crop circle 👇
type CornField = '🌽🌽🌽' | '🌽⏀🌽'; 

type RuralTown = {
    people: Array<Person>;
    cows: Array<Cow>;
    cornFields: Array<CornField>;
};

const smallville: RuralTown = {
  people: ["🧑", "🧑"],
  cows: ["🐮", "🐮", "🐮"],
  cornFields: ["🌽🌽🌽", "🌽🌽🌽"],
};
```

To conduct research, our alien scientists have created many research vessels equipped to serve specific missions. These spacecraft all study different `ResearchTarget`s.

```typescript
type ResearchTarget = {
    [researchSubject: string]: Array<string>;
}
```

`smallville`, being a `RuralTown`, is a `ResearchTarget` since it has the same shape.

Our spaceship, a `ruralTownResearchShip`, can research and interfere with any town that is like a `RuralTown`.

```typescript
const smallTownResearchShip = new ResearchShip<RuralTown>();
```

It interacts with the town through expeditions. Each time an expedition is conducted, the town may be changed in some way. The changes to the town are evaluated using an `ExpeditionChangeFn`.

```typescript
type ExpeditionChangeFn<T extends ResearchTarget> = (town: T) => T;

class ResearchShip<T extends ResearchTarget> {
    // Expeditions are executed and the resulting state of the town is returned
    public conductExpeditions(...expeditions: Array<ExpeditionChangeFn<T>>): : { on: (town: T) => T } { /*...*/ }
}
```

Our alien leaders have created a standard pattern for interacting with research targets by creating functions that produce `ExpeditionChangeFn`s. These generalized methods allow alien developers to apply the same expedition methodologies across their various spacecraft, drones, and VR technologies.

Our invasion plan will employ two of these methods using our spacecraft- `interfere` and `eradicate`.

`interfere`-ing causes us to produce a new state of how the town looks after we interact. We can provide a partial update of which features of the town changed.

```typescript
function interfere<T extends ResearchTarget>(afterInterference: Partial<T>): ExpeditionChangeFn<T> { /*...*/ }
```

For instance, if there were two corn fields previously in the town (`['🌽🌽🌽', '🌽🌽🌽']`), and we plan to draw a crop circle in one of them, we would provide that change as `interfere({ cornFields: ['🌽🌽🌽', '🌽⏀🌽'] })`.

`eradicate `-ion, meanwhile, removes all elements across all features of the town.

```typescript
function eradicate <T>(): ExpeditionChangeFn<T> { /*...*/ }
```

Eradication requires no configuration, so we can create an eradication plan with no arguments using `eradicate()`.

## Invasion Blueprint

Our invasion will be conducted in four stages. First, we will do three rounds of expeditions, and then conclude by preparing the town for our settlement.

```typescript
smallTownResearchShip
  .conductExpeditions(
    // 1️⃣, we'll make a crop circle in a corn field
    // to see how the populace reacts 👇
    interfere({ cornFields: ["🌽🌽🌽", "🌽⏀🌽"] }),
    // 2️⃣, we'll take all the cows for wildlife research 👇
    interfere({ cows: [] }),
    // 3️⃣, we'll abduct a human to analyze their weaknesses 👇
    interfere({ people: ["🧑"] }),
    // Finally, we'll eradicate everything for our colonization! 👇
    eradicate()
  )
```

Just as we've finished writing our invasion plan, we notice a type error. It looks like `interfere` is suffering from the same problem the NGXS team had with their state operators!

### Fixing Our Invasion Plan's Inferred Types

Typescript outputs the following error:

```typescript
smallTownResearchShip
  .conductExpeditions(
    //Argument of type 'ExpeditionChangeFn<{ cornFields: ("🌽🌽🌽" | "🌽⏀🌽")[]; }>' is not assignable to parameter of type 'ExpeditionChangeFn<RuralTown>'.
    //  Type '{ cornFields: ("🌽🌽🌽" | "🌽⏀🌽")[]; }' is not assignable to type 'RuralTown'. (2345)
    interfere({ cornFields: ["🌽🌽🌽", "🌽⏀🌽"] }),
    interfere({ cows: [] }),
    interfere({ people: ["🧑"] }),
    eradicate()
  )
```

This error is telling us that the inferred shape of the research target does not match the expected shape of a `RuralTown`.

The `interfere` function is inferring `T` as the shape of its first argument, `{ cornFields: ("🌽🌽🌽" | "🌽⏀🌽")[] }`. This object does not match a `RuralTown` because it is missing `cows` and `people`.

Let's look at the signature for `interfere` again.

```typescript
function interfere<T extends ResearchTarget>(afterInterference: Partial<T>): ExpeditionChangeFn<T> {
```

The research target `T` will be inferred from the parameter `afterInterference`. But that's not helpful for us, because we expect that `interfere` will return a `ExpeditionChangeFn` for the same research target shape as our spacecraft. And `RuralTown` has more properties than just `cornFields`.

So we have a strange requirement. We want to infer a generic type from context, and we want to infer based on the _return type_, not the first parameter.

This may sound like an impossible task, but Typescript has the ability to infer types based on context, known as [Contextual Typing](https://www.typescriptlang.org/docs/handbook/type-inference.html#contextual-typing).

In our case, `conductExpeditions` is typed to say that its `expeditions` argument is an array of `ExpeditionChangeFn`s which return `T`.

```typescript
conductExpeditions(...expeditions: Array<ExpeditionChangeFn<T>>)
```

Typescript knows that a function passed as an `expedition` should return a function that returns `T`. This is how Typescript correctly infers `T` for the return type of `eradicate`, actually! `eradicate`'s generic is inferable from the return type, which is contextually known to be `T` from the `conductExpeditions` signature.

In the case of the `interfere` function, Typescript is preferring to infer from the value passed into its first argument instead of contextually for the return type.

There isn't a built-in way to tell Typescript to not infer a generic type from a parameter. But, we can create a utility type for this named `NoInfer`.

```typescript
type NoInfer<T> = T extends infer S ? S : never;
```

Conditional types allow us to [`infer`](https://dev.to/aexol/typescript-tutorial-infer-keyword-2cn) the types from generics. This is helpful to infer the type of the elements of an array, for example, like:

```typescript
type ElementsOfArray<T> = T extends Array<infer E> ? E : never;
// becomes `1 | 2 | 3`
type OneTwoOrThree = ElementsOfArray<[1, 2, 3]>;
```

`NoInfer` doesn't appear to do anything at first. Since it infers `S` directly from `T`, it looks like it just repeats whatever type is passed into it:

```typescript
// becomes `string`
type StringAgain = NoInfer<string>;
```

But [the inference of conditional types is deferred in certain scenarios](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-8.html#type-inference-in-conditional-types). In this case, the condition depends on a type variable `T` to determine `S`, and the conditional type is deferred. 

So for `interfere`, that type variable is inferred from the expected return type of an element of `expeditions`!

Excited by this new knowledge, we change the definition of `interfere` to use `NoInfer`.

```typescript
// Wrap T with NoInfer 👇
function interfere<T extends ResearchTarget>(afterInterference: Partial<NoInfer<T>>): ExpeditionChangeFn<T> {
```

All our plans should now be successful, and strongly typed to boot 👾.

Let's make sure our invasion runs as expected:

```typescript
console.log('Before expeditions:', smallville);

const smallvilleAfterExpeditions = smallTownResearchShip
  .conductExpeditions(
    interfere({ cornFields: ["🌽🌽🌽", "🌽⏀🌽"] }),
    interfere({ cows: [] }),
    interfere({ people: ["🧑"] }),
    eradicate()
  )
  .on(smallville);
```

Looking at our console output, it seems our invasion went flawlessly!

```
[LOG]: "Before expeditions:",  {
  "people": [
    "🧑",
    "🧑"
  ],
  "cows": [
    "🐮",
    "🐮",
    "🐮"
  ],
  "cornFields": [
    "🌽🌽🌽",
    "🌽🌽🌽"
  ]
} 
[LOG]: "After expedition 1:",  {
  "people": [
    "🧑",
    "🧑"
  ],
  "cows": [
    "🐮",
    "🐮",
    "🐮"
  ],
  "cornFields": [
    "🌽🌽🌽",
    "🌽⏀🌽"
  ]
} 
[LOG]: "After expedition 2:",  {
  "people": [
    "🧑",
    "🧑"
  ],
  "cows": [],
  "cornFields": [
    "🌽🌽🌽",
    "🌽⏀🌽"
  ]
} 
[LOG]: "After expedition 3:",  {
  "people": [
    "🧑"
  ],
  "cows": [],
  "cornFields": [
    "🌽🌽🌽",
    "🌽⏀🌽"
  ]
} 
[LOG]: "After expedition 4:",  {
  "people": [],
  "cows": [],
  "cornFields": []
} 
```

---

I hope the lens of extraterrestrial conquest has been helpful for thinking about this weird edge of the Typescript inference system. Please examine the full example in the Typescript playground [here](https://www.typescriptlang.org/play?#code/C4TwDgpgBAShDOECGAnAxgCwCqoOYWCgF4oBvAKCigG0UFl0MBlAVwCMArCNYALinjAUASwB2uALr8AgihRIQAHkEjxAPgDc5AL5byoSFAAKEFPAD2o4lABEgPg3Ai7s2tB6AGFzAd2s3AvBuA6Hed9cHdzFFEAMWEIABsAEx9AHg3AXn2UlJsoAB9bFMAB4nS9V1gWeRisLysSCipIczAYiBk5BUUTM0tNSig0L3gm+SUPT06qHvCo2Li+qFkBxQ9x6PjO3XJyHtFBAQBbJBiYgDdhfcbi0vLPSrIu2vrT6nsnABpbRxsJJ66ez2mHgJsXn5AoD-h8vmFIkspvwHmlUskATl4ekwatgoYAKIAD0gcWEwGEljcGCQ4ggEVEiiwUAgWOAEFEU1g9FQmBwKHwwDU1gAFMAKvwsABKYjcrB6NAxJDweDMxCs5gYYRgKk0ukMplweWMdmc7nVKBgdgxYRobqWOIsHjY3H4wmbHkAOmdtNtBMs01mLRtEDx7tExNJ+ApVLUaiF-ANVEs-D5AqgwtFCa0VG01yoVDowBKVijGZjUDjlxFRDUXQzGddvrtHsddEtaAgPJ5SAAZvSUD6-fb4C8q93LC9hCX9eWKxmNls2x2uzWrv25y326ZZ-74EKU+OK5PzA1HTFzLgeQADaTLlBqt32qAAElIwigAGooABGbS8Y8vacrnHV-0bsctyzHMoG-TtfwHURNy3bQXn5YtPgrVZUx0NZJ0IeBdn2C5RC1BhMCYJUwGsUQIG8PCFUI5VFBgEo9hwtQeQA9FoAAOXMABJURW1MUNrGpWl6UZWUxB4i8mCgAB+KAJP4UiDlMPRWxYUQeGvMQOzEiBVUEjVZQonU8AIRiujArjNNMBlG34IxUAJPZFHYrixNDMsIygVd7UDMkQywUdMwIECi1EEd0wzYDwjCitnUdeDREQ8cYrM0QLLoVSIAS1NN1WbQ1mU1T-RpeQ8TQJB6R09VhLlfDsCMrkmP4TyiRJHzKT8sKIqsYLQrzTqoAAeU4bhgEdaV4GEXBRB5QCyFgmaYsGrgeEdABrCAQHgbrHV2MBm3gdgluAUKeVIGh9qGngpBoCQoG0IUhTHACstQ9YPV3CB90PHkbAAIQgVswmgBc114RFML2Q5jgaZj0J2CGjhOM8ZwgudZRIcHsIqAyCKIrpHQ2BtgCah0xw00wtJO81FkmX4bDhdJATyFFbqFTKoDJlAKdO75fhuu62Y5rnDQgOoGhhR53hZtnTCQEqyqbB6qEVqBHUsHkMchk4AKAA).