Type flowing: Rethinking the TypeScript typing system
Tags: typescript, webdev
Published: 2021-08-26 20:58:06

**Written by [Yan Sun] (https://blog.logrocket.com/author/yansun/)** ✏️

## Introduction

TypeScript provides a very rich toolbox. It includes mapped types, conditional types with control flow-based analysis, type inference, and many more.

It’s not an easy task for many JavaScript developers who are new to TypeScript to switch from loose typing to static typing. Even for developers who have been working in TypeScript for years, it can be confusing as the typing system continuously evolves.

A common myth about advanced types is that it should mainly be used for building type libraries and is not required for day-to-day TypeScript work.

The truth is that TypeScript advanced types are very useful for daily TypeScript work. They’re a great tool for building a strongly typed system into your code, expressing your intentions clearly, and making your code safer.

The purpose of introducing the type flowing concept is to think about the typing system in a way that’s similar to how we think about reactive programming data flow.

By looking at the typing system from a new perspective, it will help us to “think in types” and utilize the more advanced tools in the TypeScript toolbox in a systematic way.

### Types can flow

In reactive programming, data flows between reactive components. In the TypeScript typing system, types can flow as well.

The first time I encountered the concept of “Type flowing” was in [PranshuKhandal’s TypeScript book](https://github.com/basarat/typescript-book/blob/master/docs/types/type-inference.md). He explains this idea in the following way:

> The types flowing is just how I imagine in my brain the flow of type information.

Inspired by this, I wanted to expand the type flowing concept to the typing system level. My definition of type flowing is:

> Type flowing is when one or more subtypes are mapped and transformed from a source type. These types form a strongly constrained typing system through type operations.

The basic form of type flowing can be done via type aliases.

Type aliases allow you to create a new type name for an existing type. In the example below, the type alias `TargetType` is assigned as a reference to the `SourceType`, so the type is transferred.

```typescript
type SourceType = { id: string, quantity: number };
type TargetType= SourceType; // { id: string, quantity: number };
```

Thanks to the power of type inferences, types can flow in a few different ways. These include:

*   Through data assignment
*   With the `return type` function, which is inferred by the `return` statements; e.g., the following function is inferred to return a `number` type
*   Pattern matching with function parameters: as illustrated in the below example, annotating the `decrease` function with “`DecreaseType`” type converts the `a,b` value to a `number` type

The following code snippets illustrate the above cases.

```typescript
const quantity: number = 4;
const stockQuantity = quantity;
type StockType= typeof stockQuantity; // number

// function return type
function increase(a: number) {
    return a + 1;
}
const result = increase(2); // number

// function parameters matching
type DecreaseType = (start: number, offset: number) => number;
const decrease: DecreaseType = (a,b) => { // a: number, b: number
    return a- b;
}
```

## Reactive programming data flow vs. type flowing

The core of reactive programming is the flowing of data between the source and the reactive components. Some of its concepts are very similar to the TypeScript typing system, as you can see in the chart below.

![Comparing RxJS Operators with TypeScript Types](https://blog.logrocket.com/wp-content/uploads/2021/08/rxjs-operator-vs-typescript-type-1.png)

You can see a comparison between the RxJS operator concepts and TypeScript typing. In the typing system, the type can be transformed, filtered, and mapped to one or more subtypes.

These subtypes are “reactive” as well. When the source type changes, the subtypes will be updated automatically.

A well-designed type system will add strongly typed constraints to the data and functions in the app, so any breaking changes made to the source type definition will show an immediate compile time error.

While there are some similarities between RxJS and TypeScript typing, there are still a lot of differences between the two. For example, the data flow in RxJS occurs at runtime, while type flowing in TypeScript occurs at compile time.

The purpose of referencing RxJS here is to illustrate the flow concept in RxJS, which will hopefully help us build a shared understanding of “thinking with types.”

## Type operations in type flowing

### Map and filter

The two most frequently used operators in reactive programming are `map` and `filter`. How should we perform these two operations for types in TypeScript?

The [mapped type](https://www.typescriptlang.org/docs/handbook/2/mapped-types.html) is the equivalent of the [map operator in RxJS](https://rxjs.dev/api/operators/map). It allows us to create a type that is based on another type using the first one’s index signature and generic types.

When you combine conditional types with type inference, the type transformations you can achieve with mapped types are beyond imagination. We’ll discuss how to use mapped types later in the article.

The equivalent of arrays in type flowing is the union type. To apply filters on union types, we need to use conditional types and the `never` type. As `filter` is such a common need, TypeScript provides the `exclude` and `extract` utility types right out of the box.

The below code uses conditional types to remove types from `T` that are not assignable to `U`. The `never` type is used here for type narrowing, or filtering out the options of a union type.

```typescript
type Exclude<T, U> = T extends U ? never : T;
type T1 = Exclude<"a" | "b" | "c" , "a" >; // "b" | "c"
type Extract<T, U> = T extends U ? T: never;
type T2 = Extract<"a" | "b" | "c" , "a" >; // "a"
```

We can also filter the type properties using the out-of-the-box TypeScript utility types.

```typescript
type Omit<T, K extends string | number | symbol> = {[P in Exclude<keyof T, K>]: T[P]; }
type T1 = Omit<{ a: string, b: string }, "a"> //  { b: string; }
```

## Pipe the flow with control flow analysis

Using [control flow analysis with type guard](https://www.typescriptlang.org/docs/handbook/2/narrowing.html#control-flow-analysis), we can pipe the flow the way the pipe operator does in RxJS.

Below is an example using type guard to perform type checks, which narrow the type to a more specific one and control the logic flow.

```typescript
function doSomething(x: A | B) {
 if (x instanceof A) {
   // x is A
 } else {
   // x is B
 }
}
```

In the above example, the TypeScript compiler analyzes all possible flows of control for the expression. It looks at the `x instance of A` to determine the type of `x` to be `A` within the `if` block, and narrows the type to `B` in the `else` block.

If we think of that as the logic branching, it can be used in a connective way, similar to the way water flows through a pipe and can be redirected to a different, connected pipe to reach its destination.

## Building strong constraints using type flowing

With the theory out of the way, let’s put this idea into practice. Below, we’ll look at how the type flowing concept can be put into practice by mapping, filtering, and transforming the types to implement a well-constrained typing system.

### Defining the mapper methods

We are building a Node.js app with the mapper pattern. To implement the pattern, we must first define some mapper methods, which take data entity objects and map them to a data transfer object (DTO).

On the flip side, there is another set of methods to convert the DTOs to corresponding entity objects.

```typescript
export class myMapper {
   toClient(args: ClientEntity) : ClientDto { ...};
   fromClient(args: ClientDto) : ClientEntity{ ...};
   toOrder(args: OrderEntity) : OrderDto { ...};
   fromOrder(args: OrderDto) : OrderEntity{ ...};
}
```

We’ll use a simplified, contrived example to demonstrate the following two goals with our strong typing system:

1.  Create a type for the mapper with all the methods and interfaces from the data schema
2.  Create a union type to represent entity names for type safety

### Defining the data entity and DTO types

First, we need to define the types for the entities and DTOs.

```typescript
type DataSchema = {
  client: {
      dto:  { id: string, name: string},
      entity: {clientId: string, clientName: string}
  },
  order:  {
      dto:  { id: string, amount: number},
      entity: {orderId: string, quantity: number}
  },
}
```

Now that we have the raw data types defined, how do we extract each entity and DTO type from them?

We’ll use conditional types and `never` to filter out the required data type definitions.

```typescript
type PropertyType<T, Path extends string> = Path extends keyof T ? T[Path] : never;

type lookup<T, Key extends keyof T, prop extends string> = PropertyType<T[Key], prop>;
```

We can simplify the above by merging them into a single `lookup` type.

```typescript
type lookup<T, Key extends keyof T, prop extends string> = prop
extends keyof T[Key] ? T[Key][prop] : never;
```

### Support for nested properties

The above `lookup` type only works for a single-level property. What happens when the source type has more depth?

To access a property type with more depth, we’ll create a new type with recursive type aliases.

```typescript
type PropertyType<T, Path extends string> =
    Path extends keyof T ? T[Path] :
        Path extends `${infer K}.${infer R}` ? K extends keyof T ? PropertyType<T[K], R> : 
        never :
    never;

type lookup<T, Key, prop extends string> = Key extends keyof T? PropertyType<T[Key], prop>: never;
```

When `Path extends keyof T` is truthy, it means the full path is matched. Thus, we return the current property type.

When `Path extends keyof T` is falsy, we use the `infer` keyword to build a pattern to match the `Path`. If it matches, we make a recursive call to the next-level property. Otherwise, it will return a `never` and that means the `Path` does not match with the type.

If it does not match, continue recursively with the current property as the first parameter.

### Defining the mapper type and methods

Now, it’s time to create the mapper methods. Here, we use [string literal types](https://www.typescriptlang.org/docs/handbook/literal-types.html) to form `MapTo` and `MapFrom` with the help of the Capitalize utility type.

```typescript
// MapTo and MapFrom
type MapTo<T extends string> = `to${Capitalize<T>}`;
type MapFrom<T extends string> = `from${Capitalize<T>}`;
```

### Putting it all together

When we assemble the previous parts, our first goal is achieved!

We make use of the [key remapping](https://devblogs.microsoft.com/typescript/announcing-typescript-4-1/#key-remapping-mapped-types) feature (i.e., the `as` clause in the below code block), which has only been available since the release of TypeScript 4.1.

Please also note that `Key extends string ? Key : never` is needed because the type of object keys can vary among strings, numbers, and symbols. We are only interested in the string cases here.

```typescript
type ExtractMapperTo<T> = {
  [Key in keyof T as MapTo<Key extends string ? Key : never>]: (args: lookup<T, Key, 'dto'>) => lookup<T, Key, 'entity'>;
};

type ExtractMapperFrom<T> = {
  [Key in keyof T as MapFrom<Key extends string ? Key : never>]:(args: lookup<T, Key, 'entity'>) => lookup<T, Key, 'dto'>;
};

// Then all these mapper methods are automatically created 
type mapper = ExtractMapperTo<DataSchema> & ExtractMapperFrom<DataSchema>;
```

We can see below that all of the mapper method interfaces are automatically created.

```typescript
// Our first goal achieved!
declare const m: mapper;
m.toClient({id: '123', name: 'John'});
m.fromClient({clientId: '123', clientName: 'John'});
m.toOrder({id: '123', amount: 3});
m.fromOrder({orderId: '345',quantity: 4});
```

We have nice IDE IntelliSense support as well.

![IDE IntelliSense support ](https://blog.logrocket.com/wp-content/uploads/2021/08/ide-intellisense-support.png)

## Converting an object type to a union type

Our next goal is to create a union type to represent data type names from the source `DataSchema` type.

The key to the solution is the `PropToUnion<T>` type.

```typescript
// Derive the data type names into a union type
type PropToUnion<T> = {[k in keyof T]: k}[keyof T];

type DataTypes = PropToUnion<DataSchema>; // 'client' | 'order'
```

First, `{[k in keyof T]: k}` extracts the key of `T` as both key and value using `keyof`. The output is:

```typescript
{
   client: "client";
   order: "order";
}
```

Then, we use the index signature `[keyof T]` to extract the values as a union type.

The generated union type can help us enforce type safety. Let's say we have placed the following function in another module far away from the source type. In the `getProcessName` function, the `switch` statement triggers the type guard and `never` is returned in the default case to tell the compiler that it should never be reached.

```typescript
// Second goal achieved
function getProcessName(c: DataTypes): string {
    switch(c) {
        case 'client':
            return 'register' + c;
        case 'order':
            return 'process' + c;
        default:
        return assertUnreachable(c);
    }   
}
function assertUnreachable(x: never): never {
    throw new Error("something is very wrong");
}
```

This is how the union type and `never` help to enforce type safety.

Now, let’s assume there’s been a change to the data schema — we add a new data type called `account`. In a large team, the developer adding the new type may not be aware of the impact of the change. Without typing constraints, it could result in a hidden runtime error that is hard to find.

If we use type flowing to build the typing constraints, the downstream subtypes will be automatically updated as below.

```typescript
type DataTypes = "client" | "order" | "Account"
```

The TypeScript compiler will also show an error in the `getProcessName` function to prompt us that a breaking change has occurred.

![A compiler error due to a breaking change](https://blog.logrocket.com/wp-content/uploads/2021/08/breaking-change-error-demo.png)

Our second goal is achieved! We have a union type that now represents entity names and contributes to type safety.

To recap, this diagram shows the main steps we took to achieve the first goal of type flowing.

![A visual recap demonstrating the flow of types](https://blog.logrocket.com/wp-content/uploads/2021/08/recap-type-flow-diagram.png)

Overall, we created several new types based on the original source type. Any changes to the source type will trigger updates to all of the downstream types automatically, and we will get an instant error prompt if the change breaks the functions that depend on it.

The full example code can be found in the [Gist](https://gist.github.com/sunnyy02/7b0a768380057f2ed7541aba4d9f8d22). 

## Summary

This article discusses the TypeScript type flowing concept with reference to reactive programming in RxJS. We applied the type flowing concept to a practical example by building a well-constrained type system to maximize the benefits of type safety.

I hope this discussion helps to change the idea that TypeScript’s advanced types are only for developing type libraries or for complex framework-level programming. I also hope it can help you to start applying the typing system more creatively in your daily TypeScript work.

Happy typing!

---

## [LogRocket] (https://logrocket.com/signup/): Full visibility into your web apps

[![LogRocket Dashboard Free Trial Banner] (https://blog.logrocket.com/wp-content/uploads/2017/03/1d0cd-1s_rmyo6nbrasp-xtvbaxfg.png)](https://logrocket.com/signup/)

[LogRocket] (https://logrocket.com/signup/) is a frontend application monitoring solution that lets you replay problems as if they happened in your own browser. Instead of guessing why errors happen, or asking users for screenshots and log dumps, LogRocket lets you replay the session to quickly understand what went wrong. It works perfectly with any app, regardless of framework, and has plugins to log additional context from Redux, Vuex, and ngrx/store.

In addition to logging Redux actions and state, LogRocket records console logs, JavaScript errors, stacktraces, network requests/responses with headers + bodies, browser metadata, and custom logs. It also instruments the DOM to record the HTML and CSS on the page, recreating pixel-perfect videos of even the most complex single-page apps.

[Try it for free] (https://logrocket.com/signup/).