1 the introduction

The release of Typescript 4 Beta brings with it a number of new features, including Variadic Tuple Types that address a number of persistent problems with overloaded template code, making this update significant.

2 brief introduction

Mutable tuple type

Consider the concat scenario, which takes two array or tuple types to form a new array:

function concat(arr1, arr2) {
  return [...arr1, ...arr2];
}
Copy the code

To define the type of concat, we used to enumerate each item in the first array by enumerating:

function concat< > (arr1: [], arr2: []) :A];
function concat<A> (arr1: [A], arr2: []) :A];
function concat<A.B> (arr1: [A, B], arr2: []) :A.B];
function concat<A.B.C> (arr1: [A, B, C], arr2: []) :A.B.C];
function concat<A.B.C.D> (arr1: [A, B, C, D], arr2: []) :A.B.C.D];
function concat<A.B.C.D.E> (arr1: [A, B, C, D, E], arr2: []) :A.B.C.D.E];
function concat<A.B.C.D.E.F> (arr1: [A, B, C, D, E, F], arr2: []) :A.B.C.D.E.F];)
Copy the code

Enumerating each item in the second argument, if we were to complete all the enumerations, considering only the array length of 6, we would define 36 overloads and the code would be almost unmaintainable:

function concat<A2> (arr1: [], arr2: [A2]) :A2];
function concat<A1.A2> (arr1: [A1], arr2: [A2]) :A1.A2];
function concat<A1.B1.A2> (arr1: [A1, B1], arr2: [A2]) :A1.B1.A2];
function concat<A1.B1.C1.A2> (arr1: [A1, B1, C1], arr2: [A2]) :A1.B1.C1.A2];
function concat<A1.B1.C1.D1.A2> (arr1: [A1, B1, C1, D1], arr2: [A2]) :A1.B1.C1.D1.A2];
function concat<A1.B1.C1.D1.E1.A2> (arr1: [A1, B1, C1, D1, E1], arr2: [A2]) :A1.B1.C1.D1.E1.A2];
function concat<A1.B1.C1.D1.E1.F1.A2> (arr1: [A1, B1, C1, D1, E1, F1], arr2: [A2]) :A1.B1.C1.D1.E1.F1.A2];
Copy the code

If we use batch definition, the problem will not be solved because the order of parameter types is not guaranteed:

function concat<T.U> (arr1: T[], arr2, U[]) :Array<T | U>;
Copy the code

In Typescript 4, arrays can be deconstructed in definitions to elegantly resolve scenarios that might be reloaded hundreds of times in a few lines of code:

type Arr = readonly any[];

function concat<T extends Arr.U extends Arr> (arr1: T, arr2: U) : [...T.U] {
  return [...arr1, ...arr2];
}
Copy the code

In the above example, the Arr type tells TS that T and U are array types, and concatenates the types in logical order by […T,…U].

For example, tail returns the remaining elements after the first item:

function tail(arg) {
  const [_, ...result] = arg;
  return result;
}
Copy the code

Arr: readonly [any,…T] : readonly [any,…T] : arr: readonly [any,…T] : readonly [any,…T]

function tail<T extends any[] > (arr: readonly [any. T]) {
  const [_ignored, ...rest] = arr;
  return rest;
}

const myTuple = [1.2.3.4] as const;
const myArray = ["hello"."world"];

// type [2, 3, 4]
const r1 = tail(myTuple);

// type [2, 3, ...string[]]
const r2 = tail([...myTuple, ...myArray] as const);
Copy the code

In addition, previous versions of TS only put type deconstruction in the last place:

type Strings = [string.string];
type Numbers = [number.number];

// [string, string, number, number]
type StrStrNumNum = [...Strings, ...Numbers];
Copy the code

If you try […Strings,…Numbers] you will get an error message:

A rest element must be last in a tuple type.
Copy the code

Typescript version 4 does support this syntax:

type Strings = [string.string];
type Numbers = number[];

// [string, string, ...Array<number | boolean>]
type Unbounded = [...Strings, ...Numbers, boolean];
Copy the code

For more complex scenarios, such as the higher-order partialCall function, a degree of Currification is supported:

function partialCall(f, ... headArgs) {
  return (. tailArgs) = >f(... headArgs, ... tailArgs); }Copy the code

We can use the above property to define the first parameter type of function f as sequential […T,…U] :

type Arr = readonly unknown[];

function partialCall<T extends Arr.U extends Arr.R> (
  f: (...args: [...T, ...U]) => R,
  ...headArgs: T
) {
  return (. b: U) = >f(... headArgs, ... b); }Copy the code

The test results are as follows:

const foo = (x: string, y: number, z: boolean) = > {};

// This doesn't work because we're feeding in the wrong type for 'x'.
const f1 = partialCall(foo, 100);
/ / ~ ~ ~
// error! Argument of type 'number' is not assignable to parameter of type 'string'.

// This doesn't work because we're passing in too many arguments.
const f2 = partialCall(foo, "hello".100.true."oops");
/ / ~ ~ ~ ~ ~ ~
// error! Expected 4 arguments, but got 5.

// This works! It has the type '(y: number, z: boolean) => void'
const f3 = partialCall(foo, "hello");

// What can we do with f3 now?

f3(123.true); // works!

f3();
// error! Expected 2 arguments, but got 0.

f3(123."hello");
/ / ~ ~ ~ ~ ~ ~ ~
// error! Argument of type '"hello"' is not assignable to parameter of type 'boolean'
Copy the code

Note that const f3 = partialCall(foo, “hello”); This code, since it has not yet been executed to foo, matches only the first x:string, though y: number, z: Boolean is also mandatory, but foo has not yet been executed, so this is only the parameter collection phase. F3 (123, true) will verify the required parameters, so f3() will tell you that the number of parameters is incorrect.

Yuan set of tags

The following two function definitions are functionally the same:

function foo(. args: [string.number]) :void {
  // ...
}

function foo(arg0: string, arg1: number) :void {
  // ...
}
Copy the code

However, there are subtle differences. The following functions have a name tag for each argument, but the above destruction-defined type does not. In this case, Typescript 4 supports tuple tags:

type Range = [start: number.end: number];
Copy the code

It also supports use with deconstruction:

type Foo = [first: number, second? :string. rest:any[]].Copy the code

Class inferred the type of the member variable from the constructor

Constructors do some initialization at class instantiation, such as assigning values to member variables. In Typescript 4, assignments to member variables in constructors can directly derive the type for member variables:

class Square {
  // Previously: implicit any!
  // Now: inferred to `number`!
  area;
  sideLength;

  constructor(sideLength: number) {
    this.sideLength = sideLength;
    this.area = sideLength ** 2; }}Copy the code

We can also identify the risk of undefined if the assignment to a member variable is included in a conditional statement:

class Square {
  sideLength;

  constructor(sideLength: number) {
    if (Math.random()) {
      this.sideLength = sideLength; }}get area() {
    return this.sideLength ** 2;
    / / ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~
    // error! Object is possibly 'undefined'.}}Copy the code

If initialized in another function, TS is not automatically recognized. : Explicitly declare the type:

class Square {
  // definite assignment assertion
  // vsideLength! :number;
  //         ^^^^^^^^
  // type annotation

  constructor(sideLength: number) {
    this.initialize(sideLength);
  }

  initialize(sideLength: number) {
    this.sideLength = sideLength;
  }

  get area() {
    return this.sideLength ** 2; }}Copy the code

Short-circuit assignment syntax

Quick assignment syntax is provided for the following three short-circuit syntax:

a &&= b; // a && (a = b)
a ||= b; // a || (a = b)a ?? = b;// a ?? (a = b)
Copy the code

Catch error unknown Type

After Typescript 4.0, we can define a catch error to be of unknown type to ensure that subsequent code is written in a robust type-judging manner:

try {
  // ...
} catch (e) {
  // error!
  // Property 'toUpperCase' does not exist on type 'unknown'.
  console.log(e.toUpperCase());

  if (typeof e === "string") {
    // works!
    // We've narrowed 'e' down to the type 'string'.
    console.log(e.toUpperCase()); }}Copy the code

PS: In previous versions, catch (e: unknown) generates an error indicating that an unknown type cannot be defined for error.

Custom JSX factory

TS 4 supports the jsxFragmentFactory parameter to define Fragment factory functions:

{
  "compilerOptions": {
    "target": "esnext"."module": "commonjs"."jsx": "react"."jsxFactory": "h"."jsxFragmentFactory": "Fragment"}}Copy the code

You can also comment over the configuration of a single file:

// Note: these pragma comments need to be written
// with a JSDoc-style multiline syntax to take effect.
/ * *@jsx h */
/ * *@jsxFrag Fragment */

import { h, Fragment } from "preact";

let stuff = (
  <>
    <div>Hello</div>
  </>
);
Copy the code

The above code is compiled and parsed as follows:

// Note: these pragma comments need to be written
// with a JSDoc-style multiline syntax to take effect.
/ * *@jsx h */
/ * *@jsxFrag Fragment */
import { h, Fragment } from "preact";
let stuff = h(Fragment, null, h("div".null."Hello"));
Copy the code

Other upgrade

Other quick updates:

Improved build speed. Improved build speed for the — Incremental + –noEmitOnError scenario.

support--incremental + --noEmitParameters take effect simultaneously.

Support for the @deprecated annotation, which warns the caller with a stripper in the code when used.

Partial TS Server quick startup features that take a long time to prepare for large projects. Typescript 4 is optimized under the VSCode compiler to partially respond syntactically to currently open single files.

Optimized automatic import, now the dependencies defined in package.json dependencies field are used as a basis for automatic import, instead of traversing node_modules to import unexpected packages.

In addition, there are several Break changes:

The lib.d.ts type upgrade mainly removes the Document. origin definition.

Getters or setters that override the parent Class property will now prompt an error.

Properties deleted by delete must be optional, and an error occurs if you try to delete a required key with delete.

3 intensive reading

The biggest thing about Typescript 4 is mutable tuple types, but mutable tuple types don’t solve all problems.

In my scenario, the useDesigner function is used to customize React Hook and useSelector to support the connect Redux data stream value. It is called like this:

const nameSelector = (state: any) = > ({
  name: state.name as string});const ageSelector = (state: any) = > ({
  age: state.age as number});const App = () = > {
  const { name, age } = useDesigner(nameSelector, ageSelector);
};
Copy the code

Name and age are registered by Selector, and the internal implementation must be useSelector + reduce, but the definition of the type is tricky.

import * as React from 'react';
import { useSelector } from 'react-redux';

type Function = (. args:any) = > any;

export function useDesigner();
export function useDesigner<T1 extends Function> (
  t1: T1
) :ReturnType<T1> ;
export function useDesigner<T1 extends Function.T2 extends Function> (t1: T1, t2: T2) :ReturnType<T1> & ReturnType<T2> ;
export function useDesigner<
  T1 extends Function.T2 extends Function.T3 extends Function> (t1: T1, t2: T2, t3: T3, t4: T4,) :ReturnType<T1> &
  ReturnType<T2> &
  ReturnType<T3> &
  ReturnType<T4> &
;
export function useDesigner<
  T1 extends Function.T2 extends Function.T3 extends Function.T4 extends Function> (t1: T1, t2: T2, t3: T3, t4: T4) :ReturnType<T1> &
  ReturnType<T2> &
  ReturnType<T3> &
  ReturnType<T4> &
;
export function useDesigner(. selectors:any[]) {
  return useSelector((state) = >
    selectors.reduce((selected, selector) = > {
      return {
        ...selected,
        ...selector(state),
      };
    }, {})
  ) as any;
}
Copy the code

As you can see, I need to pass in the useDesigner parameters one by one by way of function overloading. The above example only supports three parameters. If a fourth parameter is passed in, the function definition will be invalid.

Using the TS4 example, we can avoid type overloading and support enumeration instead:

type Func = (state? :any) = > any;
type Arr = readonly Func[];

constuseDesigner = <T extends Arr>( ... selectors: T ): ReturnType<T[0]> & ReturnType<T[1]> & ReturnType<T[2]> & ReturnType<T[3]> => { return useSelector((state) => selectors.reduce((selected, selector) => { return { ... selected, ... selector(state), }; }, {}) ) as any; };Copy the code

As you can see, the biggest change is that there is no need to write four overloads, but because scenarios and concat are different, this example does not simply return […T,…U], but the result of reduce, so it is currently supported only through enumeration.

Of course, there may be a solution that can support the infinite length of input parameter type resolution without enumeration. Due to my limited level, I have not yet thought of a better solution. If you have a better solution, please let me know.

4 summarizes

Typescript 4 brings stronger typing syntax, smarter type derivation, faster builds, and better developer tool optimizations. The only Break changes in Typescript 4 don’t really affect the project.

The discussion address is: read Typescript 4 · Issue #259 · dt-fe/weekly

If you’d like to participate in the discussion, pleaseClick here to, with a new theme every week, released on weekends or Mondays. Front end Intensive Reading – Helps you filter the right content.

Pay attention to the front end of intensive reading wechat public account

Copyright Notice: Freely reproduced – Non-commercial – Non-derivative – Remain signed (Creative Commons 3.0 License)