This is what TS type filtering in React and Nextjs used to do~

Hello, everyone. I'm zero one. I'm sure you've seen all kinds of coquettish TS writing methods when reading the code written by colleagues or the code of excellent open source libraries. You can't understand it without taking some time. If it were us, you might finish any directly, but when the project volume becomes larger, you will find that these TS coquettish operations are really important, Because it can help you do static type checking very well

Today, let's introduce a fancy and practical TS type - TS type filtering, which can be seen in other open source libraries

self-introduction

TS type filtering, the English name (I took it myself) is FilterConditionally, which is its complete appearance 👇

type FilterConditionally<Source, Condition> = Pick<
  Source, 
  {
    [K in keyof Source]: Source[K] extends Condition ? K : never
  }[keyof Source]
>;

Although it seems very complex, it is actually very useful. It can filter out what you want from an object type, such as:

interface Example {
    a: string; // ✅ 
    b: string; // ✅  
    c: number; // ❌ 
    d: boolean; // ❌ 
}
type NewType = FilterConditionally<Sample, string>
/*
	NewType The final result is:
	{
		a: string;
		b: string
	}
*/

I believe you have this type of function, and you also want to understand it. It doesn't matter. Next, introduce it step by step from the inside out. You must fully understand it. If you can't understand it, spray me in the comment area (I'm talking about playing ~)

Step by step introduction

There are many knowledge points involved. I'm afraid some students who are not familiar with TS will be confused. Let's introduce some common basic knowledge points first

Appetizer

It won't take you much time. You can adjust it directly

keyof

The name of the keyword keyof is called the index type query operator. Its function is as straightforward as its literal meaning: the key value of xx

interface Example {
	a: string;
  b: string;
  c: number;
  d: boolean;
}

type Keys = keyof Example   // Equivalent to type keys = 'a' | 'B' | 'C' |'d '

You can simply understand keyof as Object.keys in JavaScript

in

The keyword in can traverse enumeration types, such as:

type Keys = 'a' | 'b' | 'c' | 'd'

type Obj = {
  [T in Keys]: string;  // Traverse Keys and assign each key to string type
}
/* Equivalent to 
	 type Obj = {
	   a: string;
	 	 b: string;
		 c: string;
		 d: string;
	 }
*/

You can simply understand in as the function of for...in in JavaScript

Conditional

The second knowledge point is conditional judgment, such as:

interface A {}

interface B extends A {}  // B inherits from A

// Does B inherit from A? If yes, it is of type number; If not, it is of type string
type C = B extends A ? number : string  // Equivalent to type C = number

// Does A inherit from B? If yes, it is of type number; If not, it is of type string
type D = A extends B ? number : string  // Equivalent to type D = string

Can you put a extends B? Number: string is simply understood as a ternary operator in JavaScript

generic paradigm

I won't introduce generics any more. If you don't know much about generics, you can read it directly TS document - Generic

Dinner begins

Just after introducing the "appetizer", strike while the iron is hot and see a simple type

type MarkUnwantedTypesAsNever<Source, Condition> ={
  [K in keyof Source]: Source[K] extends Condition ? K : never
}

In a word, the function of this type is to traverse an object type and mark the unwanted type as never

for instance 🌰

interface Example {
    a: string; // ✅ 
    b: string; // ✅  
    c: number; // ❌ 
    d: boolean; // ❌ 
}

// I only want the key of string type in the Example type, and the non string is marked as never
type MyType = MarkUnwantedTypesAsNever<Example, string>
/*
	Equivalent to:
	type MyType = {
		a: 'a';
		b: 'b';
		c: never;
		d: never;
	}
*/

Let's talk about the details a little bit. The [K in keyof Example] traverses the object type of Example, and then uses the condition to judge whether the Example [k] extends string? K: never assigns a value to the corresponding key value. Assuming that the first key value of traversal is a, then Example[K] = Example[a] = string, which is String Extensions string? ' A ': never, string must inherit from string, so there is such a result

At this time, everyone was surprised. Why did you make the type like this?? Isn't the final result we want to get a type of {a:string; b:string}? Don't worry, there are other operations behind

Let's look at a small knowledge point of index provider properties

type Value = {name: "zero2one"}["name"]  // Equivalent to type Value = "zero2one"

You can simply understand it as the value corresponding to a key of the access object in JavaScript

There is another case in TS:

type Value = {
  name: "zero2one"; 
  age: 23
}["name" | "age"]

// Equivalent to type Value = "zero2one" | 23

The key value with the value of never cannot be accessed:

type Value = {
  name: "zero2one"; 
  age: never
}["name" | "age"]

// Equivalent to type Value = "zero2one"

So let's look at more complex types

type MarkUnwantedTypesAsNever<Source, Condition> ={
  [K in keyof Source]: Source[K] extends Condition ? K : never
}[keyof Source]

We skillfully use the keyof keyword to traverse and access all interface properties

// Let me borrow the results of the example just now
type MyType = {
  	a: 'a';
		b: 'b';
		c: never;
		d: never;
}['a' | 'b' | 'c' | 'd']

/*
	Equivalent to:
	type MyType = 'a' | 'b'
*/

So far, what we have done is to filter out the key values of the desired type in the target object type

Don't worry, don't worry, it's one step away from success

The last one is Pick. This type is built-in in TS. let's have a brief understanding of its role

// Implementation of Pick type
type Pick<T, K extends keyof T> = {
    [P in K]: T[P];
}

You don't need to read its implementation in detail. You just need to know that the function of Pick is to filter out some properties specified in type T

Take a simple example:

interface A {
  a: 1;
  b: 2;
  c: 3;
  d: 4;
}

type C = Pick<A, 'a' | 'c'>  // Equivalent to type C = {A: 1; C: 3}

Yes, it's that simple. Well, you can see the final BOSS

Finally, you can filter the corresponding attributes from the Source. Back to the specific example in this article, the value in the red box in the figure has been obtained as type MyType = 'a' | 'b', so just Pick it at last

interface Example {
	a: string;
  b: string;
  c: number;
  d: boolean;
}

// Results obtained above
type MyType = 'a' | 'b'

type Result = Pick<Example, MyType>  // Equivalent to type result = {A: string; B: String}

// ----The above is equivalent to ----//

interface Example {
    a: string; // ✅ 
    b: string; // ✅  
    c: number; // ❌ 
    d: boolean; // ❌ 
}
type NewType = FilterConditionally<Sample, string>
/*
	NewType The final result is:
	{
		a: string;
		b: string
	}
*/

This is the whole process of obtaining the results at the beginning of the article

Practical application examples

As stated in the title of this article, TS type filtering is very common in many excellent open source libraries. For example, we are familiar with React:

type ElementType<P = any> = {
	[K in keyof JSX.IntrinsicElements]: P extends JSX.IntrinsicElements[K] ? K : never
}[keyof JSX.IntrinsicElements] | ComponentType<P>;

last

There are too many scenarios like TS type filtering in the open source library. I hope you can easily understand them in the future. If you read in front of the screen is the back-end, you may also see it in the back-end open source framework source code~

If this article is helpful to you, please give a praise. Thank you very much~

I'm zero one, sharing technology, not just the front end!

Keywords: Front-end React TypeScript

Added by efegue on Wed, 08 Dec 2021 05:22:45 +0200