Deep dive into typing system to solve THE ultimate riddle

TypeScript: Create a condition-based subset types | by Piotr Lewandowski | DailyJS | Medium - 图1

TypeScript: Create a condition-based subset types | by Piotr Lewandowski | DailyJS | Medium - 图2

TypeScript: Create a condition-based subset types | by Piotr Lewandowski | DailyJS | Medium - 图3

Photo by Alvaro Reyes on Unsplash

TL;DR; Source code of experiment%20%3D%3E%20Promise%3Cvoid%3E%3B%0D%0A%7D%0D%0A%0D%0A%2F%2F%20Solution%0D%0Atype%20FilterFlags%3CBase%2C%20Condition%3E%20%3D%20%7B%0D%0A%20%20%20%20%5BKey%20in%20keyof%20Base%5D%3A%20%0D%0A%20%20%20%20%20%20%20%20Base%5BKey%5D%20extends%20Condition%20%3F%20Key%20%3A%20never%0D%0A%7D%3B%0D%0Atype%20AllowedNames%3CBase%2C%20Condition%3E%20%3D%20%0D%0A%20%20%20%20%20%20%20%20FilterFlags%3CBase%2C%20Condition%3E%5Bkeyof%20Base%5D%3B%0D%0A%20%0D%0Atype%20SubType%3CBase%2C%20Condition%3E%20%3D%20%0D%0A%20%20%20%20Pick%3CBase%2C%20AllowedNames%3CBase%2C%20Condition%3E%3E%3B%0D%0A%20%0D%0A%2F%2F%20Example%20%231%0D%0Atype%20PersonRaw%20%3D%20SubType%3CPerson%2C%20string%20%7C%20number%3E%3B%0D%0A%0D%0Afunction%20usePersonRawData(person%3A%20PersonRaw)%3A%20void%20%7B%0D%0A%20%20%20%20person.id%3B%20%2F%2F%20OK%0D%0A%20%20%20%20person.load()%3B%20%2F%2F%20ERR!%0D%0A%7D%0D%0A%0D%0A%2F%2F%20Example%20%232%0D%0A%0D%0Aconst%20personApi%3A%20Person%20%3D%20%7B%0D%0A%20%20%20%20id%3A%200%2C%0D%0A%20%20%20%20name%3A%20’Jhon’%2C%0D%0A%20%20%20%20lastName%3A%20’Doe’%2C%0D%0A%20%20%20%20load%3A%20()%20%3D%3E%20Promise.resolve()%0D%0A%7D%0D%0A%0D%0AusePersonRawData(personApi)%3B%20%2F%2F%20It’s%20ok%20to%20put%20bigger%20object%20to%20smaller%20type%0D%0A%0D%0A%2F%2F%20Example%20%233%0D%0Aconst%20personMeta%3A%20PersonRaw%20%3D%20%7B%0D%0A%20%20%20%20id%3A%200%2C%0D%0A%20%20%20%20name%3A%20%22Jhon%22%2C%0D%0A%20%20%20%20lastName%3A%20%22Doe%22%20%20%20%20%0D%0A%7D%0D%0A%0D%0A%2F%2F%20%20Example%20%234%0D%0Ainterface%20PersonLoader%20%7B%0D%0A%20%20%20%20loadAmountOfPeople%3A%20()%20%3D%3E%20number%3B%0D%0A%20%20%20%20loadPeople%3A%20(city%3A%20string)%20%3D%3E%20Person%5B%5D%3B%0D%0A%20%20%20%20url%3A%20string%3B%0D%0A%7D%0D%0A%0D%0Atype%20Callable%20%3D%20SubType%3CPersonLoader%2C%20(%3A%20any)%20%3D%3E%20any%3E%0D%0A). [_Solution](#6f75).

In this article, we’re going to experiment with TypeScript 2.8 conditional and mapping types. The goal is to create a type that would filter out all keys from your interface, that aren’t matching condition.

You don’t have to know details of what mapping types are. It’s enough to know that TypeScript allows you to take an existing type and slightly modify it to make a new type. This is part of its Turing Completeness.

You can think of type as function — it takes another type as input, makes some calculations and produces new type as output. If you heard of Partial<Type> or Pick<Type, Keys>, this is exactly how they work.

Say you have a configuration object. It contains different groups of keys like IDs, Dates and functions. It may come from an API or be maintained by different people for years until it grows huge. (I know, I know, that never happens)

We want to extract only keys of a given type, such as only functions that returns Promise or something more simple like key of type number.

We need a name and definition. Let’s say: SubType<Base, Condition>

We have defined two generics by which will configure SubType:

  • Base — the interface that we’re going to modify.
  • Condition — another type, this one telling us which properties we would like to keep in the new object.

Input

For testing purposes, we have Person, which is made of different types: string, number, Function. This is our “huge object” that we want to filter out.

  1. interface **Person** {
  2. id: **number**;
  3. name: **string**;
  4. lastName: **string**;
  5. load: () => **Promise<Person>**;
  6. }

Expected outcome

For example SubType of Person based on string type would return only keys of type string:

// SubType<Person, string> 

type SubType = {  
    name: string;  
    lastName: string;  
}

TypeScript: Create a condition-based subset types | by Piotr Lewandowski | DailyJS | Medium - 图4

TypeScript: Create a condition-based subset types | by Piotr Lewandowski | DailyJS | Medium - 图5

Step 1 — Baseline

The biggest problem is to find and remove keys that doesn’t match our condition. Fortunately, TypeScript 2.8 comes with conditional types! As a little trick, we’re going to create support type for a future calculation.

type **FilterFlags<Base, Condition>** = {  
    \[Key in keyof Base\]:   
        Base\[Key\] extends Condition ? Key : never  
};

For each key, we apply a condition. Depending on the result, we set the name as the type or we put never, which is our flag for keys that we don’t want to see in the new type. It’s a special type, the opposite of any. Nothing can be assigned to it!

Look how this code is evaluated:

**FilterFlags<Person, string>**; // Step 1FilterFlags<Person, **string**>  = { // Step 2  
    id: number extends string ? 'id' : never;  
 **name: string extends string ? 'name' : never;  
    lastName: string extends string ? 'lastName' : never;**  
    load: () => Promise<Person> extends string ? 'load' : never;  
}FilterFlags<Person, **string**\> = { // Step 3  
    id: **never**;  
    name: '**name'**;  
    lastName: '**lastName'**;  
    load: **never**;  
}

Note: 'id' is not a value, but a more precise version of the string type. We’re going to use it later on. Difference between string and 'id' type:

const text: string = 'name' // OK  
const text: 'id' = 'name' // ERR

Now we’re ready to build our final object. We just use Pick, which iterates over provided key names and extracts the associated type to the new object.

type **SubType<Base, Condition>** =   
        Pick<Base, AllowedNames<Base, Condition>>

Where Pick is a built-in mapped type, provided in TypeScript since 2.1:

Pick<Person, 'id' | 'name'>;   
// equals to:  
{  
   id: number;  
   name: string;  
}

TypeScript: Create a condition-based subset types | by Piotr Lewandowski | DailyJS | Medium - 图6

TypeScript: Create a condition-based subset types | by Piotr Lewandowski | DailyJS | Medium - 图7

Summarizing all steps, we created two types that support our SubType implementation:

type **FilterFlags<Base, Condition>** = {  
    \[Key in keyof Base\]:   
        Base\[Key\] extends Condition ? Key : never  
};type **AllowedNames<Base, Condition>** =   
        FilterFlags<Base, Condition>\[keyof Base\];type **SubType<Base, Condition>** =   
        Pick<Base, AllowedNames<Base, Condition>>;

Note: This is only typing system code, can you imagine that making loops and applying if statements might be possible?

Some people prefer to have types within one expression. You ask, I provide:

type **SubType<Base, Condition>** = Pick<Base, {  
    \[Key in keyof Base\]: Base\[Key\] extends Condition ? Key : never  
}\[keyof Base\]>;

TypeScript: Create a condition-based subset types | by Piotr Lewandowski | DailyJS | Medium - 图8

TypeScript: Create a condition-based subset types | by Piotr Lewandowski | DailyJS | Medium - 图9

  1. Extract only primitive key types from JSON:
    type JsonPrimitive = SubType;// equals to:
    type JsonPrimitive = {
    id: number;
    name: string;
    lastName: string;
    }// Let’s assume Person has additional address key
    type JsonComplex = SubType;// equals to:
    type JsonComplex = {
    address: {
    street: string;
    nr: number;
    };
    }

  2. Filter out everything except functions:
    interface PersonLoader {
    loadAmountOfPeople: () => number;
    loadPeople: (city: string) => Person[];
    url: string;
    }type Callable = SubType any**>// equals to:
    type Callable = {
    loadAmountOfPeople: () => number;
    loadPeople: (city: string) => Person[];
    }

If you find any other nice use cases, show us in a comment!

TypeScript: Create a condition-based subset types | by Piotr Lewandowski | DailyJS | Medium - 图10

TypeScript: Create a condition-based subset types | by Piotr Lewandowski | DailyJS | Medium - 图11

🤔 What this solution won’t solve?

  1. One interesting scenario is to create Nullable subtype. But because string | null is not assignable to null, it won’t work. If you have an idea to solve it, let us know in a comment!
    // expected: Nullable = { city, street }
    // actual: Nullable = {}type Nullable = SubType<{
    street: string | null;
    city: string | null;
    id: string;
    }, null>

  2. RunTime filtering — Remember that types are erased during compile-time. It does nothing to the actual object. If you would like to filter out an object the same way, you would need to write JavaScript code for it.

Also, I would not recommend using Object.keys() on such a structure as runtime result might be different than given type.

TypeScript: Create a condition-based subset types | by Piotr Lewandowski | DailyJS | Medium - 图12

TypeScript: Create a condition-based subset types | by Piotr Lewandowski | DailyJS | Medium - 图13

Congratulations! Today we learned how condition and mapped types work in practice. But what’s more important, we’ve focused to solve the riddle — it’s easy to combine multiple types within one, but filtering out type from keys you don’t need? Now you know. 💪

I like how TypeScript is easy to learn yet hard to master. I constantly discover new ways to solve problems that came up in my daily duties. As follow up, I highly recommend reading advanced typing page in the documentation.

The inspiration for this post comes from a StackOverflow question asking exactly this problem. If you like solving riddles, you might also be interested in what Dynatrace is doing with the software.

If you’ve learned something new, please:

→ clap 👏 button below️ so more people can see this
follow me on Twitter (@constjs) so you won’t miss future posts:
https://medium.com/dailyjs/typescript-create-a-condition-based-subset-types-9d902cea5b8c