Deep-flatten TypeScript types with finite recursion

Floris Bernard
9 min readJul 10, 2019

Update: some of the techniques in this article might be outdated 👴

To this day I still get really kind reactions to this article 🙂 Thanks for that!

Please be aware that this article was written for older versions of TypeScript. Some of the workarounds mentioned might not be necessary anymore. For example, I recommend checking out Recursive Conditional Types in the TypeScript changelog.

I still hope you enjoy reading my article and get some inspiration for hacking around with TypeScript. So with this disclaimer out of the way, feel free to continue reading 👇

This week a colleague of mine posed an interesting TypeScript conundrum:

Can I create a mapped type that extracts all deeply nested properties from an object type into a new flattened type?

A quick search for “typescript deep flatten type” showed no obvious answers. Even page 2 of Google results showed no hope of a good solution — so the only logical conclusion to draw is that this must be madness. 🤪

Or… is it? As I had so much fun the last time I hacked together an Frankenstein solution to a TypeScript problem, I felt I should give this a go too.

The goal

Let’s define the rules of our little challenge. We want to create a mapped type that accepts an arbitrary nested JSON-like type such as this one:

/* ExampleInput.ts */

export type Model = {
foo: number;
bar: string;
baz: {
qux: Array<string>;
quux: {
quuz: number | string;
corge: boolean;
};
flob: number;
};
wobble: {
doop: string;
};
};

Note on the code snippets: there used to be CodeSandbox embeds in this article, which allowed you to view feedback from the TypeScript compiler. Unfortunately I had to temporarily replace them with plain code snippets due to an issue with Medium embeds. However, you can still play around with the code in the sandbox through this link.

The goal is to preserve the primitives and Arrays, but flatten the objects such that the properties are now on root level:

/* ExpectedOutput.ts */

export type ExpectedOutput = {
foo: number;
bar: string;
qux: Array<string>;
quuz: number | string;
corge: boolean;
flob: number;
doop: string;
};

Motivation

You might be wondering why my colleague wanted to do this. I would love to tell you, but to be honest I forgot. I’m not even sure I asked him, though I’m pretty sure he had good reasons. My reason is I just like messing around with mapped types 🤷🏽‍♂️ So let’s just jump into it

Example diagram of “it” being jumped into

Part 1: Shallow flatten

Before we dive into deep flattening a type, let’s simplify the problem by creating a shallow flatten type first. Our type Flatten<T> will be an intersection of two types:

  1. All properties of T which aren’t objects
  2. All the sub-properties T (the properties on object properties of T )

So our type Flatten will look something like this:

type Flatten<T> = NonObjectPropertiesOf<T> & SubPropertiesOf<T>;

1. Non-object properties

To find all the keys corresponding to non-object values, we’re going to use an approach similar to the mapped type from my previous article:

type NonObjectKeysOf<T> = {
[K in keyof T]: T[K] extends Array<any> ?
K :
T[K] extends object ? never : K
}[keyof T];

Note that we explicitly need to include Array<any> before we exclude all objects, because technically Arrays are also objects. Now all that’s left to do is pick these keys out of our original type:

type NonObjectPropertiesOf<T> = Pick<T, NonObjectKeysOf<T>>;

That concludes the first half of our intersection type Flatten<T>. Spoiler alert: the other half is not going to be as easy.

2. Sub-properties

Let’s first get all the values of our object, then filter them down to the ones of type object while again making the exception for Arrays.

type ValuesOf<T> = T[keyof T];
type ObjectValuesOf<T> = Exclude<
Extract<ValuesOf<T>, object>,
Array<any>
>;

We now get a union of all objects on our input type. In our example type, ObjectValuesOf<Model> will give us the union of our object properties Model['baz'] and Model['wobble'] .

A failed experiment: mapping over a union

Let’s try to map over ObjectValuesOf<Model> to get all sub-properties:

type SubPropertiesOf<T> = {
[K in keyof ObjectValuesOf<T>]: ObjectValuesOf<T>[K]
};

Let’s check the type of SubPropertiesOf<Model>:

SubPropertiesOf<Model> = {}

So this gives us an empty object type. Why? It turns out that keyof ObjectValuesOf<T> is not really what we expected:

keyof ObjectValuesOf<Model> = never?!

Hmmm… 🤔 what was this never thing again?

The never type represents the type of values that never occur.
The TypeScript Handbook

So values that represent the keys of our objects never occur? What’s going on here? Well, it turns that keyof T gives us the “union of the known, public property names of T”. In the case of the union of our baz and wobble objects, it will only give us the keys that are known to be on both these objects. As the baz object doesn’t share any keys with the wobble object, we are left with an empty union aka never. Not very useful for our situation, but it actually makes sense. You want the guarantee that keyof T only gives you known properties of T. If TypeScript were to give you a key that only existed in some cases, like the key “doop” in our example… you might be dooped into a false sense of type safety. (see what I did there?)

The intersection of a union

Ok, so mapping over ObjectValuesOf<Model> doesn’t really give us what we want. But what do we want anyway? Another way of looking at it is that we want to convert our union Model['baz'] | Model['wobble'] into the intersection Model['baz'] & Model['wobble']. Luckily, an answer on StackOverflow gives us a method to do this:

type UnionToIntersection<U> = (U extends any
? (k: U) => void
: never) extends ((k: infer I) => void)
? I
: never;

What kind of sorcery is this? For the details I recommend reading the original answer, but here is the short rundown:

  • The condition U extends any doesn’t really mean much by itself, because all things in life extend any. It’s only there because conditional types are distributive: we want to have a union of the types (k: U) => void for every type in U , instead of a single (k: U) => void for the union of all U.
  • Then we use the infer keyword to get an intersection of U. This works because we put U in a function parameter, which is a “contravariant position”. To read more about contravariance, check out this article.

Putting it together

We now have all the necessary ingredients to brew a shallow Flatten type:

/* Flatten.ts */

import { Model } from "./example-types/ExampleInput";
import {
NonObjectKeysOf,
UnionToIntersection,
ObjectValuesOf
} from "./helper-types";

type Flatten<T> = Pick<T, NonObjectKeysOf<T>> &
UnionToIntersection<ObjectValuesOf<T>>;

/* TESTING */

// this should give no errors
const flattenedModel: Flatten<Model> = {
foo: 1,
bar: "abc",
qux: ["abc"],
quux: {
quuz: 2,
corge: true
},
flob: 3,
doop: "abcd"
}; // ✅ yay, no errors!

// this should give errors
const incorrectFlattenedModel: Flatten<Model> = {
foo: 1,
// ❌ Type 'number' is not assignable to type 'string':
bar: 123,
qux: ["abc"],
// ❌ Property 'corge' is missing in type '{ quuz: number; }' but required...
quux: {
quuz: 2
},
flob: 3,
doop: "abcd"
};

This is only part of the solution though. We only flattened our Object one level down. That’s not good enough, we need to go deeper…

Part 2: Recursive types

In order to also extract the deeply nested properties, we will also need to pass our child objects through Flatten recursively. So all we need to do is pass our object properties ObjectValuesOf<T> through Flatten to make sure they are flattened as well:

For more information on recursion I highly recommend this article

Yeah… turns out the TypeScript compiler doesn’t really like self-referencing types. A quick search for recursive types may point you to a comment on the TypeScript Github with a possible solution: reference back using an interface. Maybe something like this:

type DeepFlatten<T> = Pick<T, NonObjectKeysOf<T>> &
UnionToIntersection<FlattendObjectValuesOf<T>>;
interface FlattendObjectValuesOf<T>
extends DeepFlatten<ObjectValuesOf<T>> {
}

However, this gives us an error on the interface definition 😭

❌ An interface can only extend an object type or intersection of object types with statically known members. ts(2312)

Turns out the solution using interfaces only works for static types, not generic ones. So for now, it doesn’t seem possible to write a DeepFlatten type that references itself. But do we really need that?

Part 3: Finite recursion

A recursive deep flatten would in theory be infinite: it would keep flattening until there is nothing left to flatten. But lets be real: do we really have infinite types in our TypeScript applications? Do we really have types that has object nested more than 4 levels deep? If so, how about 10 levels? 🤨 Probably not. (If you do, fight me in the comments)

So here’s what I suggest we do: instead of creating a type that references itself, we create a bunch of types that reference each other. We’ll also use the distributive conditional types (extends any ?) again to make sure our intermediate types are properly distributed:

/* deep-flatten.ts */

import { Model } from "./example-types/ExampleInput";
import {
NonObjectKeysOf,
UnionToIntersection,
ObjectValuesOf
} from "./helper-types";

type DeepFlatten<T> = T extends any
? Pick<T, NonObjectKeysOf<T>> &
UnionToIntersection<DeepFlatten2<ObjectValuesOf<T>>>
: never;

type DeepFlatten2<T> = T extends any
? Pick<T, NonObjectKeysOf<T>> &
UnionToIntersection<DeepFlatten3<ObjectValuesOf<T>>>
: never;

type DeepFlatten3<T> = T extends any
? Pick<T, NonObjectKeysOf<T>> &
UnionToIntersection<DeepFlatten4<ObjectValuesOf<T>>>
: never;

type DeepFlatten4<T> = T extends any
? Pick<T, NonObjectKeysOf<T>> &
UnionToIntersection<DeepFlatten5<ObjectValuesOf<T>>>
: never;

type DeepFlatten5<T> = T extends any
? Pick<T, NonObjectKeysOf<T>> &
UnionToIntersection<DeepFlatten6<ObjectValuesOf<T>>>
: never;

type DeepFlatten6<T> = T extends any
? Pick<T, NonObjectKeysOf<T>> &
UnionToIntersection<DeepFlatten7<ObjectValuesOf<T>>>
: never;

type DeepFlatten7<T> = T extends any
? Pick<T, NonObjectKeysOf<T>> &
UnionToIntersection<DeepFlatten8<ObjectValuesOf<T>>>
: never;

type DeepFlatten8<T> = T extends any
? Pick<T, NonObjectKeysOf<T>> &
UnionToIntersection<DeepFlatten9<ObjectValuesOf<T>>>
: never;

type DeepFlatten9<T> = T extends any
? Pick<T, NonObjectKeysOf<T>>
: UnionToIntersection<ObjectValuesOf<T>>;

/* TESTING */

// this should give no errors
const flattenedModel: DeepFlatten<Model> = {
foo: 1,
bar: "abc",
qux: ["abc"],
quuz: 2,
corge: true,
flob: 3,
doop: "abcd"
}; // ✅ yay, no errors!

// this should give an error
const incorrectFlattenedModel: DeepFlatten<Model> = {
foo: 1,
bar: "abc",
qux: ["abc"],
quuz: 2,
flob: 3,
doop: "abcd"
}; // ❌ Property 'corge' is missing in type...

Yeah I know… not the prettiest of types. But it works! If it makes you feel any better, we can give it a fancy name like “finite recursion”.

So is there nothing we can do to make it a little less verbose? As far as I can think of, only a little. We can move some of the duplication to a helper type DFBase, and then only have the recursive bit repeat. And we can abbreviate some of our repeating variables so they fit on a single line 😃

/* deep-flatten-shorter.ts */

import { Model } from "./example-types/ExampleInput";
import {
NonObjectKeysOf,
UnionToIntersection,
ObjectValuesOf
} from "./helper-types";

type DFBase<T, Recursor> = Pick<T, NonObjectKeysOf<T>> &
UnionToIntersection<Recursor>;

type DeepFlatten<T> = T extends any ? DFBase<T, DF2<ObjectValuesOf<T>>> : never;
type DF2<T> = T extends any ? DFBase<T, DF3<ObjectValuesOf<T>>> : never;
type DF3<T> = T extends any ? DFBase<T, DF4<ObjectValuesOf<T>>> : never;
type DF4<T> = T extends any ? DFBase<T, DF5<ObjectValuesOf<T>>> : never;
type DF5<T> = T extends any ? DFBase<T, DF6<ObjectValuesOf<T>>> : never;
type DF6<T> = T extends any ? DFBase<T, DF7<ObjectValuesOf<T>>> : never;
type DF7<T> = T extends any ? DFBase<T, DF8<ObjectValuesOf<T>>> : never;
type DF8<T> = T extends any ? DFBase<T, DF9<ObjectValuesOf<T>>> : never;
type DF9<T> = T extends any ? DFBase<T, ObjectValuesOf<T>> : never;

// this should give no errors
const flattenedModel: DeepFlatten<Model> = {
foo: 1,
bar: "abc",
qux: ["abc"],
quuz: 2,
corge: true,
flob: 3,
doop: "abcd"
}; // ✅ yay, no errors!

// this should give an error
const incorrectFlattenedModel: DeepFlatten<Model> = {
foo: 1,
bar: "abc",
qux: ["abc"],
quuz: 2,
flob: 3,
doop: "abcd"
}; // ❌ Property 'corge' is missing in type...

So there it is: the least ugly DeepFlatten I can think of. One might be able to use the same constructs to do other kinds of flattening. Do you have a more elegant solution? Let me know in the comments! 👇

--

--

Floris Bernard

Frontend developer and climate activist. Your favorite not otherwise specified.