We’ve seen a few TypeScript type gymnastic routines here and there, but without a comprehensive workout, we’re going to do a hard exercise that uses patterns matching, construction, recursion, and more to help improve type programming.

The high-level types we want to implement are as follows:

Its type argument is the parameter string Query String, which returns the parsed parameter object, and if there is a parameter with the same name, the value is merged.

Before we rush to achieve, let’s review the relevant types of gymnastics foundation:

Fundamentals of Type Gymnastics

Pattern matching

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

For example, extract a and B from a=b:

This pattern matching pattern has many applications in arrays, strings, functions, and so on.

Read more about this in our previous article: Pattern Matching – Routines that will Make your TS gymnastic skills explode

structure

The mapping type is used to generate the index type, and the index or index value can be modified during the generation process.

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

See the previous article for details: TS type Gymnastics: Index Type Mapping and mapping

recursive

TypeScript advanced types support recursion and can handle an indeterminable number of problems.

For example, an inversion of a string of indefinite length:

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

With a brief understanding of what pattern matching, construction, and recursion are all about, it’s time to implement the complex high-level type ParseQueryString:

Thought analysis

Suppose we have a query string: A =1&a=2&b=3&c=4.

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

Each part can be further processed to extract the key value and construct the index type. For example, a=1 can extract a and 1 through pattern matching and construct the index type {a: 1}.

This gives us four index types {a:1}, {A :2}, {b:3}, {c:4}.

You just merge it into one, and when you merge it, if you have the same key, you put it in an array.

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

The overall process looks like this:

In the first step, we do not know how many query params a=1, b=2 exist, so we recursively do pattern matching to extract them.

That’s the idea behind this high-level type.

Let’s write it in detail:

Code implementation

We do this in the order shown above, first extracting each Query Param in the Query String:

The number of query Params is uncertain, so use recursion:

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

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

The first Query Param is extracted from the local variable param declared by Infer through pattern matching, and the Rest of the string is put into Rest.

ParseParam the Param, the rest of the recursive processing, and finally merge them together, MergeParams<ParseParam, ParseQueryString>.

If the pattern match is not satisfied, the last query param is left, which is also processed with ParseParam.

Then parse each query Param separately:

Select key and value from pattern matching, and then construct an index type:

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

The syntax used to construct index types is mapping type syntax.

Let’s test this ParseParam:

Now that we have parsed each query param, we can 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 the code

The merging of two index types also constructs a new index type using the syntax of mapped types.

The key is taken from both is the key in keyof OneParam | keyof OtherParam.

Value can be divided into two cases:

  • If both index types have keys, merge them, also known as MergeValues.
  • If only one of the index types has one, take its value, namely OtherParam[key] or OneParam[key].

When you merge them, if they’re the same, you return either One, if they’re different, you merge them into an array and you return [One, Other]. If it is an array, it is a combination of arrays [One,…Other].

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

Test MergeValues:

In this way, we have implemented the entire advanced type, testing it as a whole:

This case comprehensively applies recursion, pattern extraction, and the routine of construction, which is relatively complex.

The full code can be seen in this illustration:

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 the code

conclusion

We first reviewed the routines of three types of gymnastics:

  • Pattern matching: A type matches a pattern type and extracts some of its types into local variables declared by infer
  • Construction: Constructs new index types using the syntax of the mapping type, with some modifications to the index and value
  • Recursion: When dealing with an indefinite number of types, you can do one at a time and do the rest recursively

These routines are then used to implement a complex high-level type of ParseQueryString.

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