TS type gymnastics: illustrating a complex advanced type

Let's do a difficult gymnastics today. It will comprehensively use patterns matching, construction, recursion and other routines, which is very helpful to improve the level of type programming.

The advanced types we want to implement are as follows:

Its type parameter is the parameter string query string, which will return the parsed parameter object. If there is a parameter with the same name, the values will be merged.

Don't worry about realizing it first. Let's review the relevant types of gymnastics Foundation:

Foundation of type Gymnastics

pattern matching

Pattern matching refers to matching a pattern type with a type to extract some of the types into the local variables declared by infer.

For example, extract a and b in a=b:

This pattern matching routine has many applications in array, string, function and other types.

structure

The mapping type is used to generate the index type. During the generation process, you can modify the index or index value.

For example, specify key and value to generate an index type:

recursion

TypeScript advanced types support recursion and can deal with an uncertain number of problems.

For example, the inversion of strings with uncertain length:

type ReverseStr< 
    Str extends string,
    Result extends string = '' 
> = Str extends `${infer First}${infer Rest}` 
    ? ReverseStr<Rest, `${First}${Result}`> 
    : Result;
Copy code

After simply understanding what pattern matching, construction and recursion are, you can start to implement this complex high-level type ParseQueryString:

Train of thought analysis

Suppose there is such a query string: a = 1 & A = 2 & B = 3 & C = 4.

We should first divide it into four parts: a=1, a=2, b=3 and c=4. This is extracted through the pattern matching mentioned above.

Each part can be further processed to extract key value and construct it into index type. For example, if a=1, a and 1 can be extracted through pattern matching, and then constructed into index type {a: 1}.

Thus, there are four index types {a:1}, {a:2}, {b:3}, {c:4}.

It's OK to combine them into one. If there are the same key values during the combination, they should be put into the array.

The final index type is generated: {a: [1,2], b: 3, c: 4}

The overall process is as follows:

In the first step, we don't know how many query param s are a=1 and b=2, so we need to recursively do pattern matching to extract them.

This is the implementation idea of this advanced type.

Let's write it down:

code implementation

We implement it in the order shown in the figure above. First, extract each query param in the query string:

The number of query param s is uncertain, so recursion is required:

type ParseQueryString<Str extends string>
    = Str extends `${infer Param}&${infer Rest}`
        ? MergeParams<ParseParam<Param>, ParseQueryString<Rest>> 
        : ParseParam<Str>;
Copy code

The type parameter Str is the query string to be processed.

Extract the first query param into the local variable Param declared by infer through pattern matching, and put the remaining strings into Rest.

Use ParseParam to process Param and the rest recursively. Finally, merge them together, that is, mergeparams < ParseParam < Param >, parsequerystring >.

If the pattern matching is not satisfied, it means that there is still the last query param left, which is also processed with ParseParam.

Then implement the parse of each query param:

This is to extract key and value by pattern matching, and then construct an index type:

type ParseParam<Param extends string> 
    = Param extends `${infer Key}=${infer Value}` 
        ? { [K in Key]: Value } 
        : {};
Copy code

Here, the syntax of mapping type is used to construct index type.

Let's test this ParseParam first:

After parsing each query param, merge them together:

The merged part is MergeParams:

type MergeParams<
    OneParam extends object,
    OtherParam extends object
> = {
  [Key in keyof OneParam | keyof OtherParam]: 
    Key extends keyof OneParam
        ? Key extends keyof OtherParam
            ? MergeValues<OneParam[Key], OtherParam[Key]>
            : OneParam[Key]
        : Key extends keyof OtherParam 
            ? OtherParam[Key] 
            : never
}
Copy code

The merging of the two index types is also to construct a new index type with the syntax of the mapping type.

Key is taken from both, that is, key in keyof OneParam | keyof OtherParam.

value can be divided into two cases:

  • If both index types have key s, merge values is required.
  • If there is only one index type, take its value, that is, OtherParam[key] or OneParam[Key].

When merging, if the two are the same, either one will be returned. If they are different, they will be merged into the array and returned, that is [One, Other]. If it is an array, it is the combination of arrays [one,... Other].

type MergeValues<One, Other> = 
    One extends Other 
        ? One
        : Other extends unknown[]
            ? [One, ...Other]
            : [One, Other];
Copy code

Test MergeValues:

In this way, we have realized the whole advanced type. Under the overall test:

The comprehensive application of this case to the routine of recursion, pattern matching and construction is still relatively complex.

You can see the complete code by referring to this figure:

type ParseParam<Param extends string> = 
    Param extends `${infer Key}=${infer Value}`
        ? {
            [K in Key]: Value 
        } : {};

type MergeValues<One, Other> = 
    One extends Other 
        ? One
        : Other extends unknown[]
            ? [One, ...Other]
            : [One, Other];

type MergeParams<
    OneParam extends object,
    OtherParam extends object
> = {
  [Key in keyof OneParam | keyof OtherParam]: 
    Key extends keyof OneParam
        ? Key extends keyof OtherParam
            ? MergeValues<OneParam[Key], OtherParam[Key]>
            : OneParam[Key]
        : Key extends keyof OtherParam 
            ? OtherParam[Key] 
            : never
}

type ParseQueryString<Str extends string> = 
    Str extends `${infer Param}&${infer Rest}`
        ? MergeParams<ParseParam<Param>, ParseQueryString<Rest>>
        : ParseParam<Str>;


type ParseQueryStringResult = ParseQueryString<'a=1&a=2&b=2&c=3'>;
Copy code

summary

We first reviewed the following three types of gymnastics routines:

  • Pattern matching: a type matches a pattern type and extracts some of its types into the local variables declared by infer
  • Construction: construct a new index type by mapping the syntax of the type. During the construction process, you can modify the index and value
  • Recursion: when dealing with an uncertain number of types, you can only deal with one at a time, and the rest will be done recursively

Then use these routines to implement a complex high-level type of ParseQueryString.

If you can realize this advanced type independently, it shows that you have a good grasp of the routines of these three types of gymnastics.

last

If you think this article is a little helpful to you, give it a compliment. Or you can join my development exchange group: 1025263163 learn from each other, and we will have professional technical Q & A to solve doubts

If you think this article is useful to you, please click star: http://github.crmeb.net/u/defu Thank you very much!

PHP learning manual: https://doc.crmeb.com
Technical exchange forum: https://q.crmeb.com

Keywords: PHP github TypeScript .NET crmeb

Added by gwydionwaters on Mon, 14 Feb 2022 05:54:35 +0200