Revealing Compound Types in Typescript
Tags: typescript, programming, generic
Published: 2022-11-10 16:40:30

In this post I would like to show a small generic type I use sometimes when writing Typescript to improve intellisense and my developer experience.

## The Problem

Frequently when writing [Typescript](https://www.typescriptlang.org/), I run into the following scenario:

I have a set of types that I sometimes use separately or in combination depending on the context. I make heavy use of [intellisense](https://code.visualstudio.com/docs/editor/intellisense) in my IDE to get information about the shape of data structures as I write.

```typescript
// our representative for some application user object
type User = {
  name: string
  age: number
}

type Repository = {
  name: string
  url: string
}

// github data for our app users
type GithubUser = {
  id: number
  repos: Repository[]
}

// a certain set of functions only care about core user data
const fetchUser = (): User => {
  // ...
}

// and others are concerned with finding and revealing github data
const fetchGithubUser = (): GithubUser => {
  // ...
}
```

We have a `User` and a `GithubUser` type that both have different interfaces and their own methods for fetching and processing from their respective APIs. However, there are times in my UI when they need to be combined into a single entity

With both of these separately, intellisense can give us an idea of the shape of the data structure we can expect to see.

![Intellisense for our Github User data type that appears when we hover over the type](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/m2qehs7e8i35fu5ex7hw.png)

However, if we try to make a type that combines the two, using [Typescript's (&) intersection syntax](https://www.typescriptlang.org/docs/handbook/2/objects.html#intersection-types), we find that the intellisense now only shows use the type names, and will no longer reveal their shapes to us.

```typescript
type UnifiedUser = User & GithubUser

// code where I need a unified object containing info from both
const displayUserdata = (data: UnifiedUser) => {
  // ... 
}
```

![Image highlighting the intellisense for our UnifiedUser type and how it just shows User and Github user but not their details](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ifwe2imirhhabt0l6zs5.png)

By combining multiple types, we've obscured their implementations. 

## The Solution

The solution here, is to use a small bit of Typescript's very advanced [type manipulation](https://www.typescriptlang.org/docs/handbook/2/types-from-types.html) abilities to re-expose the shape of our objects. We can do this using a combination of two concepts, [mapped types](https://www.typescriptlang.org/docs/handbook/2/mapped-types.html), and [Generics](https://www.typescriptlang.org/docs/handbook/2/generics.html)

Rather than go headlong into the full explanation of the solution, I'll present it first, and discuss how the pattern works after.

```typescript
type Spread<Type> = { [Key in keyof Type]: Type[Key] }

type UnifiedUser = Spread<User & GithubUser>
```

Here, we've made a small generic type which I've called `Spread` (there may be an better name for this), which we can pass our intersection of `User` and `GithubUser`, and we find that we can now see the object representing the combination of the two types.


![Image showing how our Spread generic type reveals all the fields of User and GithubUser combined](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/vk8ypv4v9xtltim5ukrw.png)

## How it works

Without getting too much into the weeds of the immensely deep, and admittedly, arcane syntax of the Typescript type system, what we've essentially done here is:

- The type `Spread` is parametrized, meaning it itself accepts a type as part of its implementation, similar to how a function accepts a variable to use to figure out its result. It's a type who's value depends on another type!


- We've called that type variable that goes into `Spread`, `Type`. Sometimes people use `T` or some other generic name. Like regular variables, you can call type variables anything that isn't an existing type or keyword.


- After the equal sign, in the type body we define an object whose fields are the keys of the incoming type variable, and who's values are the values associated with those fields.


- The `[Key in keyof Type]` syntax makes another type variable, `Key` that represents all the keys (fields) of the type passed in.


- And finally, `T[Key]`, passes our key variable into our type variable, to get the value associated with that key! Just like how you would say `SomeObject['somefield']` in a real JS object.


- This essentially maps the keys and values of the _entire_ intersection into a object containing both.


- The intellisense sees this new object now, and not the original types used to make the intersection.

## Unions

One nice thing about this pattern is that it works for [unions](https://www.typescriptlang.org/docs/handbook/typescript-in-5-minutes-func.html#unions) as well.

```typescript
type Arrows = "Up" | "Down" | "Left" | "Right"

type Buttons = "X" | "Y" | "A" | "B"

type Pressables = Arrows | Buttons

type SpreadPressables = Spread<Arrows | Buttons>
```

`Pressables` suffers from the same problem as our object intersection.


![A type pressable that is the union of two unions, hiding their imprementations](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/671onnikzzllwxo79hw8.png)

But `SpreadPressables` does not!

![SpreadPressable type intellisense exposing all the values of the two unions that made it](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/py1e3sgrnr02zwten7ju.png)

## Caveats

While this patterns works really well with these two use cases, it can become a little wonky otherwise.

- One notable caveat is this solution doesn't work recursively. If a field in the intersection is itself some object, this won't expose that.

- Mixing unions and intersections can also sometimes fail to spread out in a way that feels intuitive.

## In Conclusion

If the above statements don't immediately click, that's fine! Understanding and Implementing these helper types isn't trivial, which is why Typescript provides a whole suite of [Utility types](https://www.typescriptlang.org/docs/handbook/utility-types.html#handbook-content) which have useful built-ins for common transformations that people tend to use when working with TS/JS applications.

Also, there are many occasions where we _want_ the implementation details of types to be squashed, especially when designing APIs. Once Typescript does expose type info, it is extremely difficult to keep it hidden if it exists in the same file. There are more advanced Type-level data structures called [Opaque types](https://calebmer.com/tangents/opaque-types-in-typescript.html) that do just that, but opaque types go far beyond intellisense; they prevent the consumer from even _making_ an implementation of the type.

There may be techniques to expand the utility of this pattern to cover for more use cases, but that's for another time. This post was just to highlight a small scenario that the utility types didn't have an answer for, but the solution turned out to be quite close at hand with some type-level programming!

Here's a link to a [typescript playground](https://www.typescriptlang.org/play?#code/C4TwDgpgBAqgzhATlAvFA3gKClAdgQwFsIAuKOYRAS1wHNsp9bS8BXQgIyUwF9NNQkKACUIYAPZwqwcYhCoMDAsTIVqdBq0QAbVZRr0+A8NADi0gBasO8JAqw4qAEzK52XRA0RjJZURKkZOQBtAF1efkFoADFWbW1bZDREqAAyKHNgKxsET2MhAGUwb3wnAB4AFRMAPnsoYIBpCHkaKABrZvEAMygqyFCyPohG5tCoHihIkygAQW1gWPiUtCKS8pT0zOzE6v4AY3FcCignKjgwbXwQRKd8YHwFAApb+7I5hbiE3IBKVFqHKAAekBUAAdODyOJiIw9sAqIc4JMjAcjsAoF0IMA9hZllBHt8yMt-gxgWDwV5MVpcBg8EQWAByekAGkYzDIAE5xvxkQi0RisRYttZcfiyEKcnYUMScKTwaCKcAqTTvAEyGEWc4OVzeEA) where you can see all the code written here in it's entirety.

Thanks for reading :)