This article through solving the problems encountered in the actual work, layer upon layer analysis method, take you to understand the advanced characteristics of TS4.3, let’s have a look.

TypeScript, already standard on the front end, was released in version 4.3 at the end of May. As a minor iteration, there are no stunning new features at first glance. But if you’re really paying attention to TypeScript, there’s one update worth focusing on:

Template String Type Improvements

Why is it noteworthy? Take a look at three updates since TS 4.0:

Variadic Tuple Types are added in version 4.0

New Template Literal Types in version 4.1

Version 4.3 Improved Template Literal Types

And now I’m telling you that Tuple Types and Template Literal Types are actually close friends. So, have you guessed, smartly, that since TS continues to work on Tuple Types and Template Literal Types, chances are it should now be possible to do things with them that were previously impossible?

I, for my part, found out about this new feature coming with TS 4.3 back in April, and have already played around with it in the preview, solving a very interesting little problem: how to statically type all possible legal paths to object types.

Let me show you what a real problem Template Literal Types with 4.3 enhancements can solve.

Restore the problem site

Our team’s current project uses FinalForm to manage form state, but that’s not the point. The point is that one of the change methods, which is almost identical to the Lodash Set method, is not completely type safe. As a result, we had to escape with the slightly ugly as any when we wrote the relevant TS code. For an example, see the code at 👇 :

type NestedForm = {
  name: ['赵' | 'money' | 'sun' | 'li'.string];
  age: number;
  articles: {
    title: string;
    sections: string[];
    date: number;
    likes: {
      name: [string.string];
      age: number; } []; } []; }// A common API in FinalForm that has almost the same semantics as set in LoDash
interface FormApi<FormValues = Record<string, any>> {
  change: <F extends keyof FormValues>(name: F, value? : Partial<FormValues[F]>) => void } const form: FormApi<NestedForm> = form.change('age', '20') form.change('age', '20') As any escape form.change('name.0', 'liu ') form.change('articles.0. Title ', 'some string') form.change('articles.0.sections.2', <Select placeholder=" Select placeholder "onChange={Kind => { Change (' ${field}.env.${I} 'as any, {Kind}); }} >Copy the code

So the question is: can we make a method like this completely type-safe?

I’m not hiding the answer: the solution to this problem requires the 4.3 enhanced Template Literal Types and the addition of Variadic Tuple Types in version 4.0, plus some other advanced features that are already in place.

See these new and senior words, no problem a high-level TS interview question 👀 there is no. And I can really promise you that if you know what it is and why it is, you’re not going to miss TS.

Solution disassembly, from shallow to deep

The first step:Core technical support

  • Too often, the solution is already hidden in the problem

    • The type-safe part of the change method is the outermost key of the object:

      • name
      • age
      • articles
    • The parts of the object that are not type safe are the other nested paths:

      • name.0
      • name.1
      • articles.0.likes.0.age

Our goal is pretty clear: get all possible paths to the object. Maybe this is still a little fuzzy, but if I say this in a different way, you might get the idea: if I give you a binary tree, the problem is starting from the root, all the possible paths.

But what does this have to do with Template Literal Types? ! Yes, of course. Very much. We all know that articles.0.likes.0. Age is a string, but it is more of a Template String type. It is this that allows us to represent the entire nested subpath of an object at the type level.

Template Literal Types with Variadic Tuple Types

If Template Literal Types are matched with Variadic Tuple Types, you can obtain all nested subpaths of an object using some generic techniques. More on how to use generics to solve all nested subpaths of an object later.

  • Core operations

    • join

      • [‘articles’, number] => articles.${number}
  • split

    • articles.${number} => ‘[‘articles’, number]
  • Detailed operation

      • { name: { firstName: string, secondName: string }, hobby: string[] }

      • Each path is a tuple, and all paths are the union of all tuples 👇

      • [‘name’] | [hobby] | [‘name’, ‘firstName’] | [‘name’, ‘secondName’] | [‘hobby’, number]

      • Tuple can easily be converted to template String type 👇

      • name | hobby | name.firstName | name. secondName | hobby.${number}

      • How to obtain the value type of path 👇

        • For a givenname.firstNameYou can see that the corresponding value type is string
        • For a givenhobby.${number}You can see that the corresponding value type is string
  • Conclusion: Template string type and tuple type can be converted equally

Step 3: You may not understandTS advanced features

Before going into the details of generic functions, this section wants to introduce some advanced TS features that you may not be familiar with. If you feel confident, you can skip this section and go straight to generics. If you find yourself confused, come back to this section later.

1. TS type systems that you may not know

We know that the core function of TS is a static type system, but do you really understand TS type system? Let me ask you a question to test this: is the type of TS a collection of values?

This is a very interesting question, and the correct answer is that types in programming languages, with one exception, are indeed collections of values. But because of special cases, we can’t think of a type in a programming language as a collection of values. This special case, called never in TS, has no corresponding value and is used to indicate that the code will crash and exit or fall into an infinite loop. Also, never is a subtype of all types, which means that any function you write that seems safe and protected by static typing is likely to crash or loop indefinitely. Unfortunately, this unlikable possibility is legal behavior allowed by the static typing system. So static typing is not a panacea.

2. Conditional types

At the heart of most useful programs, we have to make decisions based on input.

Conditional types help describe the relation between the types of inputs and outputs.

The introduction of conditional types is the basis of TS generics. We all know that you can’t program without making decisions with conditional branches, and you’ll see if else everywhere in any real programming project.

The most common conditional branch in TS generics looks like this:

SomeType extends OtherType ? TrueType : FalseType;
Copy the code

We can do some useful things based on conditional branching. For example, if a type is an array type, return the element type of the array.

type Flatten<T> = T extends unknown[] ? T[number] : T;

// Extracts out the element type.
type Str = Flatten<string[] >;// string

// Leaves the type alone.
type Num = Flatten<number>;
// number
Copy the code
Distributive Conditional Types

When conditional types act on a generic type, they become distributive when given a union type.

In addition to making decisions with branches, programming is also inseparable from loops. After all, it is completely unrealistic to write one by one. TS generic functions do not have for or while loops in the normal sense, but distribute Conditional Types, whose functions are very similar to the map method of array. It’s just that the object is of type Union. The specific manifestation can be directly seen in the following illustration:

3. Inferring Within Conditional Types

There is also an indispensable higher-order feature about conditional types: Infer. TS’s infer ability allows us to use declarative programming methods to precisely extract the parts of a complex complex type we are interested in.

Here, we used the infer keyword to declaratively introduce a new generic type variable named Item instead of specifying how to retrieve the element type of T within the true branch.

For example, the above generics to extract array element types can be achieved with infer as follows. Isn’t it easier to infer?

type Flatten<Type> = Type extends Array<infer Item> ? Item : Type;
Copy the code

4. Recursive operations on tuple and template string type

The previous section is just a warm-up. Recursive generics in this section are the core of this article. The first step in the solution debunking has indicated that the core technical support is Variadic Tuple Types and Template Literal Types. This section introduces the recursive operations of tuple and Template String from conditional generics and Infer.

Test1 is a Tuple with a fixed length and a fixed element type, as shown below. Test1 is a Tuple with a fixed length and a fixed element type. JoinTupleToTemplateStringType is a generic function that can convert a Tuple to the Template Literal Types, The result applied to Test1 is names.${number}.firstname.lastname. Specific to the realization of JoinTupleToTemplateStringType, in addition to the type and infer the conditions of use, we also use a powerful TS generic features: recursion. If you know anything about algorithms, you know that branches and loops are at the heart of any algorithm operation, and loops are completely equivalent to recursion, meaning that any algorithm implemented with loops can theoretically be implemented with recursion, and vice versa. Most of today’s major programming languages are looping, and many of you have probably heard something like “Don’t write recursion.” But at the TS generic level, we can only implement some interesting generic functions using recursion and conditions. The following code I added a detailed note, along slowly look, don’t be afraid, will be able to understand. One thing about recursion is that it can be hard to write, but it’s often a lot easier to read (provided the individual logic is complete and there are no nested recursions).

type Test1 = ['names'.number.'firstName'.'lastName'];
// Assume that the Tuple element type you need to work with will only be a string or number
// The reason for this assumption is that the key of an object is generally a string or a number
type JoinTupleToTemplateStringType<T> = T extends [infer Single] // T is already the simplest Tuple with one element
  ? Single extends string | number // If it is a recursive basis, extract the specific type of Single
    ? `${Single}`
    : never
  // If the recursion base is not reached, continue recursing
  : T extends [infer First, ...infer RestTuple] // Exactly like javascript array deconstruction
  ? First extends string | number
    ? `${First}.${JoinTupleToTemplateStringType<RestTuple>}` // Recursive operation
    : never
  : never;
type TestJoinTupleToTemplateStringType = JoinTupleToTemplateStringType<Test1>;
Copy the code

In the recursion above, we convert a Tuple to a Template Literal Type. In the recursion below, we convert a Template Literal Type to a Tuple. The code is also commented in detail, so don’t be afraid, just take your time and you’ll be sure to understand.

type Test2 = `names.The ${number}.firstName.lastName.The ${number}`;
type SplitTemplateStringTypeToTuple<T> =
  T extends `${infer First}.${infer Rest}`
    // This branch represents the need to continue recursion
    ? First extends `The ${number}`
      ? [number. SplitTemplateStringTypeToTuple<Rest>]// Exactly like the JS array construct
      : [First, ...SplitTemplateStringTypeToTuple<Rest>]
    // This branch represents reaching the recursion base, which is either nubmer or string
    : T extends `The ${number}`
    ? [number]
    : [T];
type TestSplitTemplateStringTypeToTuple = SplitTemplateStringTypeToTuple<Test2>;
Copy the code

The last step: solve the object all nested subpathRecursive generic

Finally, the final step, the real solution, is a recursive generic AllPathsOf that solves all nested subpaths of objects. AllPathsOf isn’t complicated. It’s made up of two nested generics. The two nested generics are only seven or eight lines each. So the key step was to figure out the TuplePaths first and then lay them out. One of the smoothing steps we’ve shown before is to convert a Tuple to a Template Literal Type using a recursive generic. So there is only one problem: how to extract all the child paths of the object and represent them as Tuple unions. RecursivelyTuplePaths themselves are not complicated either. The RecursivelyTuplePaths are annotated in the following code. Don’t be afraid to take your time and be sure to understand them.

The rest is ValueMatchingPath, look at the code seems to be a bit more complex than AllPathsOf, but because it is only an additional function, here is not detailed, interested in the code can see, I believe that after the baptism of the previous several rounds of recursive generics, this slightly longer is not a problem.

 //
 // Supported environment: TS 4.3+
 //

 /** Get all the child paths of the nested object */
type AllPathsOf<NestedObj> = object extends NestedObj
  ? never
  // Organize all subpaths into tuple union, and then flattens each tuple into Template Literal Type
  : FlattenPathTuples<RecursivelyTuplePaths<NestedObj>>;

 /** For the stator path and nested object, get the value type */ corresponding to the subpath
export type ValueMatchingPath<NestedObj, Path extends AllPathsOf<NestedObj>> =
  string extends Path
    ? any
    : object extends NestedObj
    ? any
    : NestedObj extends readonly (infer SingleValue)[] / / Array
    ? Path extends `The ${string}.${infer NextPath}`
      ? NextPath extends AllPathsOf<NestedObj[number] >// Path is nested, continue recursion
        ? ValueMatchingPath<NestedObj[number], NextPath>
        : never
      : SingleValue // Path is not nested. The item type of the array is the target result
    : Path extends keyof NestedObj / / Record
    ? NestedObj[Path] // If Path is one of the keys of Record, the target result can be returned directly
    : Path extends `${infer Key}.${infer NextPath}` // Otherwise continue the recursion
    ? Key extends keyof NestedObj
      ? NextPath extends AllPathsOf<NestedObj[Key]> // Enter recursion through two levels of judgment
        ? ValueMatchingPath<NestedObj[Key], NextPath>
        : never
      : never
    : never;

 /** * Recursively convert objects to tuples, like * `{ name: { first: string } }` -> `['name'] | ['name', 'first']` */
type RecursivelyTuplePaths<NestedObj> = NestedObj extends (infer ItemValue)[] / / Array
  // The Array case returns a number and continues the recursion
  ? [number"|"number. RecursivelyTuplePaths<ItemValue>]// Exactly like the JS array constructor
  : NestedObj extends Record<string.any> / / Record
  ?
      // The record case needs to return the key at the outermost layer of the record and continue the recursion
      | [keyof NestedObj]
      | {
          [Key in keyof NestedObj]: [Key, ...RecursivelyTuplePaths<NestedObj[Key]>];
        }[Extract<keyof NestedObj, string>]
        // It's a little complicated here, but what we're doing is essentially constructing an object, and value is the tuple we want
        // Finally extract value
  // If it is neither an array nor a record, the primitive type is encountered, the recursion ends, and an empty tuple is returned.
  : [];

 /** * Flatten tuples created by RecursivelyTupleKeys into a union of paths, like: * `['name'] | ['name', 'first' ] -> 'name' | 'name.first'` */
type FlattenPathTuples<PathTuple extends unknown[]> = PathTuple extends[]?never
  : PathTuple extends [infer SinglePath] // Note that [string] is a Tuple
  ? SinglePath extends string | number // Extract Path type based on condition judgment
    ? `${SinglePath}`
    : never
  : PathTuple extends [infer PrefixPath, ...infer RestTuple] // Is the syntax similar to array deconstruction?
  ? PrefixPath extends string | number // Continue recursion through conditional judgment
    ? `${PrefixPath}.${FlattenPathTuples<Extract<RestTuple, (string | number) [] > >}`
    : never
  : string;

 /** * Modify the change method in the FormApi Interface with the new template String type enhancement in TS 4.3, and the usability is almost perfect **/
interface FormApi<FormValues = Record<string, any>> {
  change: <Path extends AllPathsOf<FormValues>>( name: Path, value? : Partial<ValueMatchingPath<FormValues, Path>> ) => void; } / / demonstration of nested Form type interface NestedForm {name: [' zhao '|' money '|' sun '|' li ', string]; age: number; articles: { title: string; sections: string[]; date: number; likes: { name: [string, string]; age: number; } []; } []; } // pretend to have a NestedForm instance of the change method const change: FormApi<NestedForm>['change'] = (name, value) => { console.log(name, value); }; // 👇 let index = 0; change(`articles.0.likes.${index}.age`, 10); Change (` name ${index} `, 'liu'); 🤔 /** extract all subpaths, Here intuitive show * / type AllPathsOfNestedForm = | keyof NestedForm | ` name. ${number} ` | ` articles. ${number} ` | `articles.${number}.title` | `articles.${number}.sections` | `articles.${number}.date` | `articles.${number}.likes` | `articles.${number}.sections.${number}` | `articles.${number}.likes.${number}` | `articles.${number}.likes.${number}.name.${number}` | `articles.${number}.likes.${number}.age` | `articles.${number}.likes.${number}.name`;Copy the code

The last step:Use tail recursion techniques to optimize the performance of generic functions

The final step is bonus, extra optimizations. You can see that AllPathsOf is a recursion with a lot of runtime complexity. This should be a common problem with recursion, and some of my friends don’t like recursion because of this. But the problem of recursion can be circumvented by technical means. This technique is called tail recursion.

Let’s use the classic Fibonacci sequence to really feel the difference between recursion, tail recursion, and loop:

 // Fibonacci is a recursive version of Fibonacci
function fibRecursive(n: number) :number {
  return n <= 1 ? n : fibRecursive(n - 1) + fibRecursive(n - 2);
}

 // Fibonacci is a recursive version of Fibonacci
function fibTailRecursive(n: number) {
  function fib(a: number, b: number, n: number) :number {
    return n === 0 ? a : fib(b, a + b, n - 1);
  }
  return fib(0.1, n);
}

 // Loop Fibonacci is similar to tail recursive Fibonacci.
function fibLoop(n: number) {
  let [a, b] = [0.1];
  for (let i = 0; i < n; i++) {
    [a, b] = [b, a + b];
  }
  return a;
}
Copy the code

Yes, tail recursion has the same performance in time complexity as loops.

Here’s how tail recursion can be used in TS generics:

type OneLevelPathOf<T> = keyof T & (string | number)
type PathForHint<T> = OneLevelPathOf<T>;

The P argument is a state container that holds the result of each step of the recursion and ultimately helps us achieve the tail recursion
type PathOf<T, K extends string, P extends string = ' '> =
  K extends `${infer U}.${infer V}`
    ? U extends keyof T  // Record
      ? PathOf<T[U], V, `${P}${U}. `>
      : T extends unknown[]  // Array
      ? PathOf<T[number], V, `${P}The ${number}. `>
      : `${P}${PathForHint<T>}`  // Go to this branch, indicating that the parameter is wrong, prompting the user to correct the parameter
    : K extends keyof T
    ? `${P}${K}`
    : T extends unknown[]
    ? `${P}The ${number}`
    : `${P}${PathForHint<T>}`;  // Go to this branch, indicating that the parameter is wrong, prompting the user to correct the parameter

 /** * Use the change method in the FormApi interface to improve performance ** /
interface FormApi<FormValues = Record<string, any>> {
  changeName: PathOf<FormValues, Path>, value? : Partial<ValueMatchingPath<FormValues, Path>> ) => void; }Copy the code

conclusion

The TS 4.3 Template Literal Types practice ends here. These slightly complex but logical recursive generics can be a bit difficult to understand, and if they are, that’s fine. We can take our time later. But this level of recursive generics is necessary to truly master TS, so this article does have some value 👀 😊

Refer to the link

Github.com/microsoft/T…