2

Added Awaited type

Awaited can extract the actual return type of Promise. According to the name, it can be understood as: waiting for the type obtained after the Promise resolves. The following is the Demo provided by the official documentation:

 // A = string
type A = Awaited<Promise<string>>;

// B = number
type B = Awaited<Promise<Promise<number>>>;

// C = boolean | number
type C = Awaited<boolean | Promise<number>>;

Bundled dom lib types can be replaced

Because of its out-of-the-box features, TS bundles all dom built-in types. For example, we can directly use the Document type, which is provided by TS built-in.

Maybe sometimes you don't want to upgrade the associated dom built-in type with the TS version upgrade, so TS provides a solution to specify the dom lib type, which can be declared in package.json @typescript/lib-dom :

 {
 "dependencies": {
    "@typescript/lib-dom": "npm:@types/web"
  }
}

This feature improves the environmental compatibility of TS, but in general, it is recommended to use it out of the box, eliminating the need for tedious configuration and better maintenance of the project.

Template string types also support type narrowing

 export interface Success {
    type: `${string}Success`;
    body: string;
}

export interface Error {
    type: `${string}Error`;
    message: string;
}

export function handler(r: Success | Error) {
    if (r.type === "HttpSuccess") {
        // 'r' has type 'Success'
        let token = r.body;
    }
}

The template string type has been supported for a long time, but now it is supported to narrow the type according to the template string in the branch condition.

Add new --module es2022

While you can use --module esnext to keep up to date with features, if you want to use stable version numbers and support top-level await features, you can use es2022.

tail recursion optimization

The TS type system supports tail recursion optimization. Take the following example to understand:

 type TrimLeft<T extends string> =
    T extends ` ${infer Rest}` ? TrimLeft<Rest> : T;

// error: Type instantiation is excessively deep and possibly infinite.
type Test = TrimLeft<"                                                oops">;

Before tail recursion optimization, TS will report an error because the stack is too deep, but now it can return the execution result correctly, because after tail recursion optimization, it will not form a gradually deepening call, but exit the current function immediately after execution, The number of stacks always remains the same.

JS has not yet achieved automatic tail recursion optimization, but it can be simulated through a custom function TCO. The implementation of this function is given below:

 function tco(f) {
  var value;
  var active = false;
  var accumulated = [];
  return function accumulator(...rest) {
    accumulated.push(rest);
    if (!active) {
      active = true;
      while (accumulated.length) {
        value = f.apply(this, accumulated.shift());
      }
      active = false;
      return value;
    }
  };
}

The core is to turn recursion into a while loop, so that no stack is generated.

Force retention of imports

TS will kill the unused imports when compiling, but this time the --preserveValueImports parameter is provided to disable this feature, because the following conditions will cause the import to be removed by mistake:

 import { Animal } from "./animal.js";

eval("console.log(new Animal().isDangerous())");

Because TS can't distinguish references in eval, similar to vue's setup syntax:

 <!-- A .vue File -->
<script setup>
import { someFunc } from "./some-module.js";
</script>

<button @click="someFunc">Click me!</button>

Support variable import type declaration

Variables referenced by the following syntax tags were previously supported as types:

 import type { BaseType } from "./some-module.js";

Variable-level type declarations are now supported:

 import { someFunc, type BaseType } from "./some-module.js";

This makes it easy to safely erase BaseType when building an independent module, because when a single module is built, it cannot perceive the content of the some-module.js file, so if it is not specified type BaseType , the TS compiler will not recognize it as a type variable.

class private variable check

Contains two features, the first is that TS supports the inspection of class private variables:

 class Person {
    #name: string;
}

The second is to support the judgment of #name in obj , such as:

 class Person {
    #name: string;
    constructor(name: string) {
        this.#name = name;
    }

    equals(other: unknown) {
        return other &&
            typeof other === "object" &&
            #name in other && // <- this is new!
            this.#name === other.#name;
    }
}

This judgment implicitly requires that #name in other of other is an object instantiated by Person, because this syntax can only exist in a class, and it can be further narrowed down to the Persion class.

Import Assertion

The import assertion proposal is supported:

 import obj from "./something.json" assert { type: "json" };

and assertions for dynamic imports:

 const obj = await import("./something.json", {
    assert: { type: "json" }
})

TS This feature supports any type of assertion, regardless of whether the browser recognizes it. So if the assertion is to take effect, it needs either of the following two supports:

  • Browser support.
  • Build script support.

However, at present, the syntax supported by build scripts is not uniform. For example, Vite asserts the import type in the following two ways:

 import obj from "./something?raw"

// 或者自创的语法 blob 加载模式
const modules = import.meta.glob(
  './**/index.tsx',
  {
    assert: { type: 'raw' },
  },
);

Therefore, the import assertion can at least unify the syntax of the build tool in the future, and even after the browser supports it natively, there is no need for the build tool to process the import assertion.

In fact, there is still a long way to go completely relying on browser parsing, because a complex front-end project has at least 3000~5000 resource files, and it is impossible to use bundles to load these resources one by one in the production environment, because the speed is too slow.

const read-only assertion

 const obj = {
  a: 1
} as const

obj.a = 2 // error

All attributes of the object specified by this syntax are readonly .

Faster loading with realpathSync.native

I don't know much about developers, just use realpathSync.native to improve the TS loading speed.

Fragment auto-completion enhancements

The automatic completion function of Class member functions and JSX properties has been enhanced. After using the latest version of TS, you should already have a sense of movement. For example, after entering a carriage return in a JSX writing tag, it will automatically complete the content according to the type, such as:

 <App cla />
//    ↑回车↓
//        <App className="|" />
//                        ↑光标自动移到这里

Code can be written before super()

The restriction of JS on super() is that this cannot be called before, but the TS restriction is more strict. Any code written before super() will report an error, which is obviously too strict.

Now TS has relaxed the verification policy, only calling this before super() will report an error, and executing other codes is allowed.

In fact, this should have been changed a long time ago. Such a strict verification policy made me think that JS would not allow any function to be called before super() , but I thought it was unreasonable, because super() that the constructor function of the parent class is called. The reason why it is not called automatically, but needs to be called manually super() is that developers can flexibly decide which logic is executed before the constructor of the parent class, so The one-size-fits-all behavior of TS actually caused super() to lose its meaning and become a meaningless template code.

Type narrowing also works for destructuring

This feature is really powerful, that is, the type narrowing still takes effect after destructuring.

Previously, the narrowing of TS types was already very powerful, and the following judgments could be made:

 function foo(bar: Bar) {
  if (bar.a === '1') {
    bar.b // string 类型
  } else {
    bar.b // number 类型
  }
}

But if a and b are deconstructed from bar in advance, they cannot be automatically narrowed. Now the problem has also been solved, and the following code can also work normally:

 function foo(bar: Bar) {
  const { a, b } = bar
  if (a === '1') {
    b // string 类型
  } else {
    b // number 类型
  }
}

Deep recursive type checking optimization

The following assignment statement produces an exception because the type of the property prop does not match:

 interface Source {
    prop: string;
}

interface Target {
    prop: number;
}

function check(source: Source, target: Target) {
    target = source;
    // error!
    // Type 'Source' is not assignable to type 'Target'.
    //   Types of property 'prop' are incompatible.
    //     Type 'string' is not assignable to type 'number'.
}

This is easy to understand. From the point of view of the error report, TS will also find the prop type mismatch according to the recursive detection method. But since TS supports generics, the following is an example of infinite recursion:

 interface Source<T> {
    prop: Source<Source<T>>;
}

interface Target<T> {
    prop: Target<Target<T>>;
}

function check(source: Source<string>, target: Target<number>) {
    target = source;
}

In fact, it doesn't need to be as complicated as the official description says, even props: Source<T> is enough to make the example recurse infinitely. In order to ensure that there is no error in this situation, TS makes a recursion depth judgment. Too deep recursion will terminate the judgment, but this will bring a problem, that is, the following errors cannot be recognized:

 interface Foo<T> {
    prop: T;
}

declare let x: Foo<Foo<Foo<Foo<Foo<Foo<string>>>>>>;
declare let y: Foo<Foo<Foo<Foo<Foo<string>>>>>;

x = y;

In order to solve this problem, TS made a judgment: recursive protection only takes effect in the scenario of recursive writing, and the above example, although it is also a deep recursion, but because it is written by a person, TS will also take the trouble. Recursively go down one by one, so the scene can work correctly.

The core of this optimization is that TS can analyze the recursion caused by "very abstract/heuristic" writing according to the code structure, which is the recursion caused by enumeration one by one, and exempt the latter from the recursion depth check.

Enhanced index deduction

The examples given in the official documents below look complicated at first glance, so let's disassemble and analyze them:

 interface TypeMap {
    "number": number;
    "string": string;
    "boolean": boolean;
}

type UnionRecord<P extends keyof TypeMap> = { [K in P]:
    {
        kind: K;
        v: TypeMap[K];
        f: (p: TypeMap[K]) => void;
    }
}[P];

function processRecord<K extends keyof TypeMap>(record: UnionRecord<K>) {
    record.f(record.v);
}

// This call used to have issues - now works!
processRecord({
    kind: "string",
    v: "hello!",

    // 'val' used to implicitly have the type 'string | number | boolean',
    // but now is correctly inferred to just 'string'.
    f: val => {
        console.log(val.toUpperCase());
    }
})

The purpose of this example is to achieve processRecord function, which pass parameters by identifying kind automatically derive callback f in value of type.

For example kind: "string" , then val is a string type, kind: "number" , then val .

Because this update of TS solves the problem of the type of val that could not be recognized before, we don't need to care how TS solves it, just remember that TS can correctly identify the scene (a bit like the formula of Go, for the classic example It's best to learn one by one), and understand how the scene is constructed.

How to do it? First define a typemap:

 interface TypeMap {
    "number": number;
    "string": string;
    "boolean": boolean;
}

Then define the final function processRecord :

 function processRecord<K extends keyof TypeMap>(record: UnionRecord<K>) {
    record.f(record.v);
}

A generic K is defined here, K extends keyof TypeMap is equivalent to K extends 'number' | 'string' | 'boolean' , so here is the value range of the following generic K, which is one of the three strings.

The point is that the parameter record needs to be determined according to the incoming kind f callback function parameter type. Let's first imagine how to write the following UnionRecord type:

 type UnionRecord<K extends keyof TypeMap> = {
  kind: K;
  v: TypeMap[K];
  f: (p: TypeMap[K]) => void;
}

As above, the natural idea is to define a generic K, so kind and f , p types can be represented, so processRecord<K extends keyof TypeMap>(record: UnionRecord<K>) The UnionRecord<K> means that the current received actual type K is passed in UnionRecord , so that UnionRecord knows what type is actually processed.

This function has ended when I came here, but the official definition UnionRecord is slightly different:

 type UnionRecord<P extends keyof TypeMap> = { [K in P]:
    {
        kind: K;
        v: TypeMap[K];
        f: (p: TypeMap[K]) => void;
    }
}[P];

This example has deliberately increased the complexity, and used the index method to go around it. Maybe TS could not parse this form before. In short, this writing method is now supported. Let's see why this writing is equivalent to the above. The above writing is simplified as follows:

 type UnionRecord<P extends keyof TypeMap> = { 
  [K in P]: X
}[P];

It can be interpreted as, UnionRecord defines a generic type P, and the function obtains the type from the object { [K in P]: X } according to the index (or understood as a subscript) [P] . And [K in P] This type definition describing the Key value of an object is equivalent to defining multiple types. Since it happens to be P extends keyof TypeMap , you can understand that the type is expanded like this:

 type UnionRecord<P extends keyof TypeMap> = { 
  'number': X,
  'string': X,
  'boolean': X
}[P];

And P is a generic type. Because of the definition of [K in P] , it must be able to hit one of the above, so it is actually equivalent to the following simple writing:

 type UnionRecord<K extends keyof TypeMap> = {
  kind: K;
  v: TypeMap[K];
  f: (p: TypeMap[K]) => void;
}

Parametric Control Flow Analysis

The literal translation of this feature is quite strange, let’s understand it from the code:

 type Func = (...args: ["a", number] | ["b", string]) => void;

const f1: Func = (kind, payload) => {
    if (kind === "a") {
        payload.toFixed();  // 'payload' narrowed to 'number'
    }
    if (kind === "b") {
        payload.toUpperCase();  // 'payload' narrowed to 'string'
    }
};

f1("a", 42);
f1("b", "hello");

If you define parameters as arrays and use or concatenate enumerations, you potentially include a runtime type narrowing. For example, when the value of the first parameter is a , the type of the second parameter is determined to be number , and the value of the first parameter is b . The parameter type is determined as string .

It is worth noting that this type of type deduction is from front to back, because the parameters are passed from left to right, so the front is deduced from the front, and the front cannot be derived from the back (for example, it cannot be understood that the second parameter is number type, the value of the first parameter must be a ).

Remove unnecessary code generated by JSX compilation

JSX compiles the last meaningless void 0 , reducing the code size:

 - export const el = _jsx("div", { children: "foo" }, void 0);
+ export const el = _jsx("div", { children: "foo" });

Since the changes are small, you can take the opportunity to learn how to modify the TS source code. This is the PR DIFF address .

It can be seen that the modification location is the src/compiler/transformers/jsx.ts file, and the change logic is to remove the factory.createVoidZero() function, which, as its name, will create the end void 0 , except In addition, there are a large number of tests file modifications. In fact, it is not difficult to understand the source code context.

JSDoc validation prompt

Since JSDoc comments are separated from the code, it is easy to fork with the actual code as you continue to iterate:

 /**
 * @param x {number} The first operand
 * @param y {number} The second operand
 */
function add(a, b) {
    return a + b;
}

TS can now give hints for inconsistencies in naming, types, etc. By the way, try not to use JSDoc when TS is used, after all, there is a risk of inconsistency between code and type separation at any time.

Summarize

Judging from these two updates, TS has entered a mature stage, but TS is still in the early stage on the issue of generic classes. There are a large number of complex scenarios that cannot be supported, or there is no elegant compatible solution. I hope that it can continue to improve and complex in the future. The type of scene supported.

The discussion address is: Intensive Reading "New Features of Typescript 4.5-4.6" Issue #408 dt-fe/weekly

If you'd like to join the discussion, click here , there are new topics every week, with a weekend or Monday release. Front-end intensive reading - help you filter reliable content.

Follow Front-end Intensive Reading WeChat Official Account

<img width=200 src="https://img.alicdn.com/tfs/TB165W0MCzqK1RjSZFLXXcn2XXa-258-258.jpg">

Copyright notice: Free to reprint - non-commercial - non-derivative - keep attribution ( Creative Commons 3.0 license )

黄子毅
7k 声望9.5k 粉丝