TypeScript has been released in the 4.6 beta at 2022.1.21, you can view all included issues and PR at Milestone 4.6. To get a head start on the new feature, do:

$ npm install typescript@beta
Copy the code

Install the beta version of TypeScript, or install JavaScript and TypeScript Nightly in VS Code to update the built-in TypeScript support.

This is my second TypeScript update blog post. The previous one, “TypeScript 4.5 release: New extensions, new syntax, new tool types,” can be found in this account creation. Based on the experience of the previous article, the author will further improve the description style of the article, including the historical background, practical application and appropriate expansion of some features. I will continue to update TypeScript DevBlog as well. Thank you for reading.

Review of previous version

  • TypeScript version 4.5 is an important release for NodeJS, introducing new compilerOptions.module configuration: Node12 and Nodenext (es2022 with top-level await), Support for types & exports & imports resolution in package.json, new file extensions.mts and.cts (products correspond to.mjs and.cjs)

  • New tool types are used to extract internal Promise values and replace a number of related Promise internal declaration definitions, such as the new Promise.all definition:

    interface PromiseConstructor {
      all<T extends readonly unknown[] | []>(values: T): PromiseThe < {-readonly [P in keyof T]: Awaited<T[P]> }>;
    }
    Copy the code
  • Type guards based on template string types:

    export interface Success {
      type: `The ${string}Success`;
      body: string;
    }
    
    export interface Error {
      type: `The ${string}Error`;
      message: string;
    }
    
    export function isSuccess(r: Success | Error) :r is Success {
      return r.type === "HttpSuccess"
    }
    Copy the code
  • Mixing value imports with type imports:

    import { Foo, type BarType } from 'lib';
    Copy the code
  • Other features, such as tail-recursion Elimination on Conditional Types, Import Assertions, Assertions from TC39 proposal-import-assertions, currently at Stage 3)

Compared to version 4.5, version 4.6 brings fewer new features (13 Major Changes are officially listed in version 4.5, compared to 7 in version 4.6), and the focus is back on the type system. Includes control flow analysis enhancements and index access type derivation that will be highlighted this time.

Allows for Code in Constructors Before super() to be executed easilysuper())

We know that Class in ES6 requires that the constructor of a derived Class must make a super() call to ensure that this has been initialized. TypeScript keeps this constraint and goes one step further. Code before super() is not allowed at all in some cases, even if this is not used in previous code.

The Super call in TypeScript Spec is defined like this:

Constructors of non-derived classes may have no super calls, while constructors of derived classes must have (at least one) super calls, and super calls are not allowed inside the constructor, outside the constructor. The call to the super statement must be on the first line of the constructor if both of the following conditions are true:

  • Derived classes
  • Constructors declare parameter properties, or instance property declarations of classes with initializers.

In the following cases:

class Foo {
  constructor(name: string){}}class Bar extends Foo {
  someProperty = true;

  constructor(name: string) {
    // Error: A 'super' call must be the first statement in the constructor when a class contains initialized properties, parameter properties, or private identifiers.(2376)
    const transformed = transformer(name);
    super(transformed); }}const transformer = (arg: string) = > {
  return "linbudu";
};
Copy the code

This is actually a common scenario where the parameters need to be checked/converted before calling super. If this is simple, we can bypass the validation directly with Super (Transformer (ARgs)), but if we want to turn each step on separately, Super (Transformer (validator(foo(args)))))? Code that does not use this should not throw an error when called before super (in fact, ES6 supports this).

Simply put, this feature allows code that does not reference this to be executed before super calls, making the constructors of OOP libraries that rely heavily on OOP a little cleaner.

Improved Recursion Depth Checks

We know that TypeScript’s Type System is a “duck Type,” or Structural Type System, which means that the following code holds true:

interface Source {
  prop: "foo" | "bar";
}

interface Target {
  prop: "foo" | "bar";
}

function check(source: Source, target: Target) {
  target = source;
}
Copy the code

It is permissible to assign the Source structure to the type body of the Target structure here, because the structured type system determines the compatibility of the two structures by comparing their internal properties to whether they are compatible (or allocatable). If we change the property (prop) in the structure of the Target, which is the assigned type, so that it is no longer assignable from source.prop, the assignment will throw an error:

interface Source {
  prop: "foo" | "bar";
}

interface Target {
  prop: "foo";
}

function check(source: Source, target: Target) {
  // error!
  // Type 'Source' is not assignable to type 'Target'.
  // Types of property 'prop' are incompatible.
  // Type 'foo' | 'bar is not assignable to type 'foo'.
  // Type 'bar is not assignable to type 'foo'.
  target = source;
}
Copy the code

We’re going to use the combined types of straightforward literal types as examples here, so the whole thing looks pretty straightforward. So, what if we introduced generics?

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

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

function check(source: Source<"foo" | "bar">, target: Target<"foo" | "bar">) {
  target = source;
}
Copy the code

Can you still intuitively tell whether this is allowed or not? Let’s go ahead and compare the prop of the two structures in the same way as the structure-type system, Source> and Target

>, and you’ll see that this is an infinite loop.

TypeScript’s current logic for dealing with this situation is to perform some kind of deep expansion checking, and if you’re not done deciding that it’s an infinite loop, consider the two types compatible, called heuristic recursive type checking.

This is usually enough, but there are a few things that can slip through the net:

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

We can clearly determine that the assignment should not be valid because y is nested one less layer, but previous versions of TypeScript do not report errors. For recursive type checking before, it focuses on the nested structure of situation (both in the example above, the structure of the attribute of the reference structure for dolls, or this case, the structure of the generic parameters in reference to the structure of dolls), and not the actual statement, directly after certain depth checks that two types of compatible.

This feature was introduced in TypeScript 4.5.3, so this can be checked for errors later in TypeScript 4.5.3.

Version 4.6 further enhanced recursive type checking to distinguish between explicitly specified special cases where structures are referenced in generic parameters that no longer concern the structure, and cases where infinite nesting can be identified more quickly. Obviously, some types checking of DefinitelyTyped Package reduces the cost by more than 50%, such as @types/yup and @types/redux-immutable. If you are interested in the implementation, refer to #46599.

Indexed Access type Inference Inference Improvements

Indexed Access Types allow you to retrieve the type represented by a key value from the key name of the interface, e.g. :

type Person = { age: number; name: string; alive: boolean };
// number
type Age = Person["age"];

// number | string
type NameOrAge = Person["age" | "name"];

// number | string | boolean
type All = Person[keyof Person]
Copy the code

Some “index” related concepts can cause confusion, such as:

  • The index type
  • Index signature type
  • Index access
  • keyof
  • Mapping type

I covered these concepts in detail in the previous article “The other side of TypeScript: Type Programming” (also at this account), but I’ll make a brief distinction here.

  • Index type: An index type is not a specific type (like a mapping type), but rather a collective name for a series of index-based type operations, including index signature types, index access, and so on.

  • Index signature type: used to quickly establish an interface with consistent internal field types, or to provide access support for unknown attributes of the interface:

    // equivalent to Record
            ,>
    interface Foo {
      [keys: string] :string;
    }
    
    // Avoid errors when accessing Bar['job'], common in refactoring scenarios or dynamic interface structures
    interface Bar {
      name: string;
      age: number;
      [keys: string]: unknown;
    }
    
    type Job = Bar['job'] as AcutallyJobType
    Copy the code
  • Index access, that is, access the type of interface by index (see the example above), or access arrays, tuples by index:

    const stringArr = <const> ["lin"."bu"."du"];
    
    / / "Lin" | "bu" | "du", if removed as const statement, is a string
    type TypeFromArr = typeof stringArr[number];
    
    const tuple = <const> ["linbudu".true.18];
    
    / / true | | "linbudu" 18, if removed as const statement for Boolean | string | number
    type TypeFromTuple = typeof tuple[number];
    Copy the code

    Person[‘age’] = Person[‘age’] = Person[‘age’];

    type AgeLiteralType = "age";
    
    type AgeType = Person[AgeLiteralType];
    Copy the code
  • Keyof vs. mapping type: Keyof, like Typeof, is a separate type operator, not an index type. This is used to obtain the combined type of all key literals in a structure, as in:

    // "name" | "age" | "alive"
    type PersonProps = keyof Person;
    Copy the code

    It can access the type of the key only if it is used with the mapping type.

    A mapping type is similar to the Map method of JavaScript arrays. It is commonly used in utility types to do various things based on existing interfaces, such as mapping all types of an interface to a string:

    interface A {
      a: boolean;
      b: string;
      c: number;
      d: () = > void;
    }
    
    type StringifyA<T> = {
      [K in keyof T]: string;
    };
    Copy the code

    Note that the mapping type and keyof are independent of each other, but collocation works wonders.

    A more common scenario is to use with index type access of the form T[k] :

    type Clone<T> = {
      [K in keyof T]: T[K];
    };
    Copy the code

These related concepts can cause some confusion, but when taken seriously, there are very clear differences between them.

Back to the index access type derivation in version 4.6, before the index access type actually has some type derivation capability, but there are still many shortcomings, see the following example:

type UnionRecord =
  | { kind: "n"; v: number; f: (v: number) = > void }
  | { kind: "s"; v: string; f: (v: string) = > void }
  | { kind: "b"; v: boolean; f: (v: boolean) = > void };

type VTypes = UnionRecord["v"]
Copy the code

Here VTypes can be correctly string is derived | number | Boolean, but the result is derived in the following will result in an error:

function processRecord(rec: UnionRecord) {
    rec.f(rec.v);  // Error, 'string | number | boolean' not assignable to 'never'
}
Copy the code

Since the type of the parameter rec will only be a branch of the union type, the v and f types should be corresponding, but v is still the union type and f becomes (v: Never) = > void (never comes from the ‘string | number | Boolean’ and sets, and obviously they didn’t set). The result of this check means that the check is being performed to see if the V in each record can be assigned to the F in each record as a parameter, which is not what we expect.

Prior to version 4.6 we were actually able to solve this problem as well, where the unexpected result was clearly from the fact that the various type branches of the union type were not isolated. We could use generics to explicitly correct the type system’s control flow analysis, where only one type branch was available at a time:

type RecordTypeMap = { n: number; s: string; b: boolean };

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

function processRecord<K extends keyof RecordTypeMap> (rec: UnionRecord<K>) {
  rec.f(rec.v); // Ok
}
Copy the code

If kind is n, v must be number and f must be (v: number) => void.

The introduction of this feature allows us to eliminate the need to use the above code, and also to obtain precise type hints in the indexed type mapped types, i.e. by branching.

Control Flow Analysis for Dependent Parameters is supported

While writing this article, I also wrote a TypeScript control flow analysis article. If you’re not familiar with the concept of control flow analysis, I recommend reading the article, “Evolution of Control flow Analysis in TypeScript: With new features in version 4.6 as a reference, we will only give a brief introduction here.

This feature is actually similar to the template string type-based type guard introduced in version 4.5, which is essentially template string type control flow analysis support:

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

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

function response(r: Success | Error) {
  if (r.type === "HttpSuccess") {
    return r.body;
  }
  if (r.type === "HttpError") {
    return r.message;
  }

  return null;
}
Copy the code

In contrast, the support of the control flow analysis of parameter types is actually the support of the discernible joint type derivation of parameter types.

Here’s a new concept: Discriminated Unions or Tagged Unions. It’s actually not that complicated, as we’ve already seen examples above:

type UnionRecord =
  | { kind: "n"; v: number; f: (v: number) = > void }
  | { kind: "s"; v: string; f: (v: string) = > void }
  | { kind: "b"; v: boolean; f: (v: boolean) = > void };
Copy the code

First of all, we know that a recognizable union type must be a union type, so how do we evolve it to be recognizable? There really only needs to be one property in each type branch, and the type in each branch is different (it can be basic, advanced, literal, etc., it just needs to be different, but it is generally recommended to use literal types), Call the Discriminant Property or Tagged Property.

Secondly, generally, when playing a role of association type, we need to narrow it ** to a branch of the type. The best way is to play a role of identifying association attributes. Typeof (if (type === ‘success’)), typeof (if (typeof obj.kind === ‘string’), etc.

This feature focuses on identifying parameter types (we use tuples directly), such as:

type Args = ['a'.number"|"'b'.string];

const f1: Func = (kind, payload) = > {};
Copy the code

In the case above, the union type is narrowed down to the corresponding part by Kind’s judgment, which is used in the same way as the common type guard:

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

In the first if block, due to the corresponding joint type identification, the type of payload can be narrowed to number. The analysis process behind this is control flow analysis, as described in another article.

New performance analysis tool TypeScript Trace Analyzer

TypeScript Compiler provides the –generateTrace option to generate the Compiler’s percentage of time spent in this compilation. Performance Tracing can be visualized by using Edge/Chrome support for Performance Tracing.

This release of Trace Analyzer will be used to display reports in a more intuitive and clear way (if it doesn’t work, skip the graph).

Disruptive change

Removes non-generic types from object deconstruction

When we use the deconstruction assignment and REST expansion operators on a generic object (such as a class), methods in the object and non-destructible values with getters and setters are removed from the REST type:

class Thing {
    someProperty = 42;

    someMethod() {
        // ...}}function foo<T extends Thing> (x: T) {
    let{ someProperty, ... rest } = x; rest.someMethod(); }Copy the code

Here, the type of the rest will be deduction of the right to Omit < T, “someProperty” | “someMethod” > (the previous is Omit < T, “someProperty” >, but actually someMethod is not to spread, That is x.someMethod, but not rest.somemethod).

Syntax checking of JavaScript files

This is actually one of the major new features in version 4.6 (More Syntax and Binding Errors in JavaScript)

Now, include JavaScript files in TypeScript with syntax errors such as duplicate declarations, use of modifiers for export declarations, and default cases that appear multiple times in switch case statements.

Similar to TypeScript files, you can disable type checking with @ts-nocheck.

conclusion

This concludes the major new features of TypeScript 4.6 Beta. Due to the rigor of the TypeScript team, beta and official releases are usually only minor tweaks. So you can basically say that this is the main feature of TypeScript 4.6. In terms of release intervals, it is usually a month and a half between beta releases and official releases based on past experience.

See you in TypeScript 4.7.