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:

All properties of T which aren’t objects 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 .

doesn’t really mean much by itself, because all things in life extend . It’s only there because conditional types are distributive: we want to have a union of the types for every type in , instead of a single for the union of all . 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:

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…