21

精读《Typescript 4》

 4 years ago
source link: https://segmentfault.com/a/1190000023200181
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

1 引言

随着 Typescript 4 Beta 的发布,又带来了许多新功能,其中 Variadic Tuple Types 解决了大量重载模版代码的顽疾,使得这次更新非常有意义。

2 简介

可变元组类型

考虑 concat 场景,接收两个数组或者元组类型,组成一个新数组:

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

如果要定义 concat 的类型,以往我们会通过枚举的方式,先枚举第一个参数数组中的每一项:

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];)

再枚举第二个参数中每一项,如果要完成所有枚举,仅考虑数组长度为 6 的情况,就要定义 36 次重载,代码几乎不可维护:

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];

如果我们采用批量定义的方式,问题也不会得到解决,因为参数类型的顺序得不到保证:

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

在 Typescript 4,可以在定义中对数组进行解构,通过几行代码优雅的解决可能要重载几百次的场景:

type Arr = readonly any[];

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

上面例子中, Arr 类型告诉 TS TU 是数组类型,再通过 [...T, ...U] 按照逻辑顺序依次拼接类型。

再比如 tail ,返回除第一项外剩下元素:

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

同样告诉 TS T 是数组类型,且 arr: readonly [any, ...T] 申明了 T 类型表示除第一项其余项的类型,TS 可自动将 T 类型关联到对象 rest

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);

另外之前版本的 TS 只能将类型解构放在最后一个位置:

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

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

如果你尝试将 [...Strings, ...Numbers] 这种写法,将会得到一个错误提示:

A rest element must be last in a tuple type.

但在 Typescript 4 版本支持了这种语法:

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

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

对于再复杂一些的场景,例如高阶函数 partialCall ,支持一定程度的柯里化:

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

我们可以通过上面的特性对其进行类型定义,将函数 f 第一个参数类型定义为有顺序的 [...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);
}

测试效果如下:

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'

值得注意的是, const f3 = partialCall(foo, "hello"); 这段代码由于还没有执行到 foo ,因此只匹配了第一个 x:string 类型,虽然后面 y: number, z: boolean 也是必选,但因为 foo 函数还未执行,此时只是参数收集阶段,因此不会报错,等到 f3(123, true) 执行时就会校验必选参数了,因此 f3() 时才会提示参数数量不正确。

元组标记

下面两个函数定义在功能上是一样的:

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

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

但还是有微妙的区别,下面的函数对每个参数都有名称标记,但上面通过解构定义的类型则没有,针对这种情况,Typescript 4 支持了元组标记:

type Range = [start: number, end: number];

同时也支持与解构一起使用:

type Foo = [first: number, second?: string, ...rest: any[]];

Class 从构造函数推断成员变量类型

构造函数在类实例化时负责一些初始化工作,比如为成员变量赋值,在 Typescript 4,在构造函数里对成员变量的赋值可以直接为成员变量推导类型:

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

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

如果对成员变量赋值包含在条件语句中,还能识别出存在 undefined 的风险:

class Square {
  sideLength;

  constructor(sideLength: number) {
    if (Math.random()) {
      this.sideLength = sideLength;
    }
  }

  get area() {
    return this.sideLength ** 2;
    //     ~~~~~~~~~~~~~~~
    // error! Object is possibly 'undefined'.
  }
}

如果在其他函数中初始化,则 TS 不能自动识别,需要用 !: 显式申明类型:

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

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

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

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

短路赋值语法

针对以下三种短路语法提供了快捷赋值语法:

a &&= b; // a = a && b
a ||= b; // a = a || b
a ??= b; // a = a ?? b

catch error unknown 类型

Typescript 4.0 之后,我们可以将 catch error 定义为 unknown 类型,以保证后面的代码以健壮的类型判断方式书写:

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());
  }
}

PS:在之前的版本, catch (e: unknown) 会报错,提示无法为 error 定义 unknown 类型。

自定义 JSX 工厂

TS 4 支持了 jsxFragmentFactory 参数定义 Fragment 工厂函数:

{
  "compilerOptions": {
    "target": "esnext",
    "module": "commonjs",
    "jsx": "react",
    "jsxFactory": "h",
    "jsxFragmentFactory": "Fragment"
  }
}

还可以通过注释方式覆盖单文件的配置:

// 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>
  </>
);

以上代码编译后解析结果如下:

// 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"));

其他升级

其他的升级快速介绍:

构建速度提升,提升了 --incremental + --noEmitOnError 场景的构建速度。

支持 --incremental + --noEmit 参数同时生效。

支持 @deprecated 注释, 使用此注释时,代码中会使用 删除线 警告调用者。

局部 TS Server 快速启动功能,打开大型项目时,TS Server 要准备很久,Typescript 4 在 VSCode 编译器下做了优化,可以提前对当前打开的单文件进行部分语法响应。

优化自动导入,现在 package.json dependencies 字段定义的依赖将优先作为自动导入的依据,而不再是遍历 node_modules 导入一些非预期的包。

除此之外,还有几个 Break Change:

lib.d.ts 类型升级,主要是移除了 document.origin 定义。

覆盖父 Class 属性的 getter 或 setter 现在都会提示错误。

通过 delete 删除的属性必须是可选的,如果试图用 delete 删除一个必选的 key,则会提示错误。

3 精读

Typescript 4 最大亮点就是可变元组类型了,但可变元组类型也不能解决所有问题。

拿笔者的场景来说,函数 useDesigner 作为自定义 React Hook 与 useSelector 结合支持 connect redux 数据流的值,其调用方式是这样的:

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);
};

nameage 是 Selector 注册的,内部实现方式必然是 useSelector + reduce,但类型定义就麻烦了,通过重载可以这么做:

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;
}

可以看到,笔者需要将 useDesigner 传入的参数通过函数重载方式一一传入,上面的例子只支持到了三个参数,如果传入了第四个参数则函数定义会失效,因此业界做法一般是定义十几个重载,这样会导致函数定义非常冗长。

但参考 TS4 的例子,我们可以避免类型重载,而通过枚举的方式支持:

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

const useDesigner = <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;
};

可以看到,最大的变化是不需要写四遍重载了,但由于场景和 concat 不同,这个例子返回值不是简单的 [...T, ...U] ,而是 reduce 的结果,所以目前还只能通过枚举的方式支持。

当然可能存在不用枚举就可以支持无限长度的入参类型解析的方案,因笔者水平有限,暂未想到更好的解法,如果你有更好的解法,欢迎告知笔者。

4 总结

Typescript 4 带来了更强类型语法,更智能的类型推导,更快的构建速度以及更合理的开发者工具优化,唯一的几个 Break Change 不会对项目带来实质影响,期待正式版的发布。

讨论地址是: 精读《Typescript 4》· Issue #259 · dt-fe/weekly

如果你想参与讨论,请 点击这里 ,每周都有新的主题,周末或周一发布。前端精读 - 帮你筛选靠谱的内容。

关注 前端精读微信公众号

JVN7rum.jpg!web

版权声明:自由转载-非商用-非衍生-保持署名( 创意共享 3.0 许可证

本文使用 mdnice 排版


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK