TypeScript: Create a condition-based subset types

<em>TL;DR;&nbsp;</em><a href="https://www.typescriptlang.org/play/#src=interface%20Person%20%7B%0D%0A%20%20%20%20id%3A%20number%3B%0D%0A%20%20%20%20name%3A%20string%3B%20%20%20%20%0D%0A%20%20%20%20lastName%3A%20string%3B%0D%0A%20%20%20%20load%3A%20%28%29%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%28person%3A%20PersonRaw%29%3A%20void%20%7B%0D%0A%20%20%20%20person.id%3B%20%2F%2F%20OK%0D%0A%20%20%20%20person.load%28%29%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%27Jhon%27%2C%0D%0A%20%20%20%20lastName%3A%20%27Doe%27%2C%0D%0A%20%20%20%20load%3A%20%28%29%20%3D%3E%20Promise.resolve%28%29%0D%0A%7D%0D%0A%0D%0AusePersonRawData%28personApi%29%3B%20%2F%2F%20It%27s%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%28%29%20%3D%3E%20number%3B%0D%0A%20%20%20%20loadPeople%3A%20%28city%3A%20string%29%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%28_%3A%20any%29%20%3D%3E%20any%3E%0D%0A" target="_blank"><em>Source code of experiment</em></a><em>.&nbsp;</em><a href="https://medium.com/dailyjs/typescript-create-a-condition-based-subset-types-9d902cea5b8c#6f75" target="_blank"><em>Solution</em></a><em>.</em>

TL;DR; Source code of experimentSolution.

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.

📐Let’s define the problem

Say you have a configuration object. It contains different groups of keys like IDsDates 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.

interface Person {
    id: number;
    name: string;    
    lastName: string;
    load: () => Promise<Person>;
}

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;
}

📈Step by step to a solution

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 1
FilterFlags<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



Step 2 — List of keys that match type condition

At this point, we have done our crucial work! Now we have a new objective: Gather the names of keys that passed our validation. For SubType<Person, string>, it would be: 'name' | 'lastName'.

type AllowedNames<Base, Condition> =
FilterFlags<Base, Condition>[keyof Base]

We’re using the code from the previous step and adding only one more part: [keyof Base] 

What this does is gather the most common types of given properties and ignore never (as those can’t be used anyway).

type family = {
type: string;
sad: never;
members: number;
friend: 'Lucy';
}
family['type' | 'members'] // string | number
family['sad' | 'members'] // number (never is ignored)
family['sad' | 'friend'] // 'Lucy'

Above, we have an example of returning string | number. So how can we get names? In the first step we replaced the type of key with its name!

type FilterFlags = {
name: 'name';
lastName: 'lastName';
id: never;
}
AllowedNames<FilterFlags, string>; // 'name' | 'lastName'


We’re close to a solution now.



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;
}

🎉Full Solution

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]>;

🔥 Usages

  1. Extract only primitive key types from JSON:


type JsonPrimitive = SubType<Person, number | string>;
// equals to:
type JsonPrimitive = {
id: number;
name: string;
lastName: string;
}
// Let's assume Person has additional address key
type JsonComplex = SubType<Person, object>;
// 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<PersonLoader, (_: any) => any>
// equals to:
type Callable = {
loadAmountOfPeople: () => number;
loadPeople: (city: string) => Person[];
}

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

🤔 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.

Summary

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.

🙏 Please give some claps by clicking on the clap 👏🏻 button below if you enjoyed this post.‍‍🧞

You can talk with me on twitter: @constjs or follow for other advanced programming issues!


By : Piotr Lewandowski


Learn TypeScript | TypeScript Crash Course | TypeScript Tutorial for Beginners

Learn TypeScript | TypeScript Crash Course | TypeScript Tutorial for Beginners

Learn TypeScript | TypeScript Crash Course | TypeScript Tutorial for Beginners: My goal with this courses is just give your the fundamentals of the language, show you what TypeScript is and how to use it. We as developers don't have time neither can we afford to spend too much time on any tehcnology.

TypeScript is a superset of JavaScript, which means that is language that was created to add features to JavaScript. You might be asking yourself why it was created and why not just add those features directly to JavaScript.

Sometimes language foundatons take time to implement features because the committee has to approve it, test and get feedback before requests are answered. The soultion for that is to create another language that adds functionality to the language we need, and thats where TypesScript comes in.

TypeScript has many advanced features that plain JavaScript doesnt have yet but the good news is that we can start using it now since TypeScript compiles JavaScript ES5 which at this moment is the most compatible version of JavaScript for all browsers.

Most people that want to learn TypeScript its because they need the skills to use with some Frameworks like Angular.

My goal with this courses is just give your the fundamentals of the language, show you what TypeScript is and how to use it. We as developers don't have time neither can we afford to spend too much time on any tehcnology.

Thanks for reading

If you liked this post, please do share/like it with all of your programming buddies!

Follow us on Facebook | Twitter

Further reading about JavaScript and TypeScript

The Complete JavaScript Course 2019: Build Real Projects!

Vue JS 2 - The Complete Guide (incl. Vue Router & Vuex)

JavaScript Bootcamp - Build Real World Applications

The Web Developer Bootcamp

New ES2019 Features Every JavaScript Developer Should Know

Best JavaScript Frameworks, Libraries and Tools to Use in 2019

Using Typescript with modern React (i.e. hooks, context, suspense)

WebSocket + Node.js + Express — Step by step tutorial using Typescript

From Javascript to Typescript to Elm

Angular + Typescript = Powerful Web Apps

React + TypeScript : Why and How

How to use TypeScript with Vue.js

Introduction To TypeScript

Some tools are so useful that once you've used them in one project, it is hard to work on other projects without them. React was certainly that way for me when I first started, and now TypeScript has become the same way. This talk will cover why typed JavaScript, especially in large codebases, is worth the added effort. I will discuss my personal experiences - the good, the bad, and the ugly - and talk about why I can't seem to start a project without <code>npm install typescript</code>.

Some tools are so useful that once you've used them in one project, it is hard to work on other projects without them. React was certainly that way for me when I first started, and now TypeScript has become the same way. This talk will cover why typed JavaScript, especially in large codebases, is worth the added effort. I will discuss my personal experiences - the good, the bad, and the ugly - and talk about why I can't seem to start a project without npm install typescript.


Learn more


Understanding TypeScript

http://learnstartup.net/p/HkVUp2y8e

Typescript Masterclass & FREE E-Book

http://learnstartup.net/p/BJKmIUFP2g

Angular Tutorial: Create a CRUD App with Angular CLI and TypeScript

http://dev.thegeeknews.net/0774add31e

Introduction to TypeScript Development

http://learnstartup.net/p/HyEiI-lWx

6 Essential VSCode Extensions for Angular Developers

http://go.learn4startup.com/8d20682b41

Angular Essentials (Angular 2+ with TypeScript)

http://learnstartup.net/p/SkdU19JFZ

Backend API with Node, Express & TypeScript - Fast Track

http://learnstartup.net/p/rtgZjWg_g

Learn Protractor(Angular Automation) from scratch +Framework

http://learnstartup.net/p/fiGYxoVhe