## 1 引言 随着 [Typescript 4 Beta](https://devblogs.microsoft.com/typescript/announcing-typescript-4-0-beta/) 的发布,又带来了许多新功能,其中 Variadic Tuple Types 解决了大量重载模版代码的顽疾,使得这次更新非常有意义。 ## 2 简介 ### 可变元组类型 考虑 `concat` 场景,接收两个数组或者元组类型,组成一个新数组: ```typescript function concat(arr1, arr2) { return [...arr1, ...arr2]; } ``` 如果要定义 `concat` 的类型,以往我们会通过枚举的方式,先枚举第一个参数数组中的每一项: ```typescript function concat<>(arr1: [], arr2: []): [A]; function concat(arr1: [A], arr2: []): [A]; function concat(arr1: [A, B], arr2: []): [A, B]; function concat(arr1: [A, B, C], arr2: []): [A, B, C]; function concat(arr1: [A, B, C, D], arr2: []): [A, B, C, D]; function concat(arr1: [A, B, C, D, E], arr2: []): [A, B, C, D, E]; function concat(arr1: [A, B, C, D, E, F], arr2: []): [A, B, C, D, E, F];) ``` 再枚举第二个参数中每一项,如果要完成所有枚举,仅考虑数组长度为 6 的情况,就要定义 36 次重载,代码几乎不可维护: ```typescript function concat(arr1: [], arr2: [A2]): [A2]; function concat(arr1: [A1], arr2: [A2]): [A1, A2]; function concat(arr1: [A1, B1], arr2: [A2]): [A1, B1, A2]; function concat( arr1: [A1, B1, C1], arr2: [A2] ): [A1, B1, C1, A2]; function concat( arr1: [A1, B1, C1, D1], arr2: [A2] ): [A1, B1, C1, D1, A2]; function concat( arr1: [A1, B1, C1, D1, E1], arr2: [A2] ): [A1, B1, C1, D1, E1, A2]; function concat( arr1: [A1, B1, C1, D1, E1, F1], arr2: [A2] ): [A1, B1, C1, D1, E1, F1, A2]; ``` 如果我们采用批量定义的方式,问题也不会得到解决,因为参数类型的顺序得不到保证: ```typescript function concat(arr1: T[], arr2, U[]): Array; ``` 在 Typescript 4,可以在定义中对数组进行解构,通过几行代码优雅的解决可能要重载几百次的场景: ```typescript type Arr = readonly any[]; function concat(arr1: T, arr2: U): [...T, ...U] { return [...arr1, ...arr2]; } ``` 上面例子中,`Arr` 类型告诉 TS `T` 与 `U` 是数组类型,再通过 `[...T, ...U]` 按照逻辑顺序依次拼接类型。 再比如 `tail`,返回除第一项外剩下元素: ```typescript function tail(arg) { const [_, ...result] = arg; return result; } ``` 同样告诉 TS `T` 是数组类型,且 `arr: readonly [any, ...T]` 申明了 `T` 类型表示除第一项其余项的类型,TS 可自动将 `T` 类型关联到对象 `rest`: ```typescript function tail(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 只能将类型解构放在最后一个位置: ```typescript type Strings = [string, string]; type Numbers = [number, number]; // [string, string, number, number] type StrStrNumNum = [...Strings, ...Numbers]; ``` 如果你尝试将 `[...Strings, ...Numbers]` 这种写法,将会得到一个错误提示: ```text A rest element must be last in a tuple type. ``` 但在 Typescript 4 版本支持了这种语法: ```typescript type Strings = [string, string]; type Numbers = number[]; // [string, string, ...Array] type Unbounded = [...Strings, ...Numbers, boolean]; ``` 对于再复杂一些的场景,例如高阶函数 `partialCall`,支持一定程度的柯里化: ```typescript function partialCall(f, ...headArgs) { return (...tailArgs) => f(...headArgs, ...tailArgs); } ``` 我们可以通过上面的特性对其进行类型定义,将函数 `f` 第一个参数类型定义为有顺序的 `[...T, ...U]`: ```typescript type Arr = readonly unknown[]; function partialCall( f: (...args: [...T, ...U]) => R, ...headArgs: T ) { return (...b: U) => f(...headArgs, ...b); } ``` 测试效果如下: ```typescript 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()` 时才会提示参数数量不正确。 ### 元组标记 下面两个函数定义在功能上是一样的: ```typescript function foo(...args: [string, number]): void { // ... } function foo(arg0: string, arg1: number): void { // ... } ``` 但还是有微妙的区别,下面的函数对每个参数都有名称标记,但上面通过解构定义的类型则没有,针对这种情况,Typescript 4 支持了元组标记: ```typescript type Range = [start: number, end: number]; ``` 同时也支持与解构一起使用: ```typescript type Foo = [first: number, second?: string, ...rest: any[]]; ``` ### Class 从构造函数推断成员变量类型 构造函数在类实例化时负责一些初始化工作,比如为成员变量赋值,在 Typescript 4,在构造函数里对成员变量的赋值可以直接为成员变量推导类型: ```typescript class Square { // Previously: implicit any! // Now: inferred to `number`! area; sideLength; constructor(sideLength: number) { this.sideLength = sideLength; this.area = sideLength ** 2; } } ``` 如果对成员变量赋值包含在条件语句中,还能识别出存在 `undefined` 的风险: ```typescript class Square { sideLength; constructor(sideLength: number) { if (Math.random()) { this.sideLength = sideLength; } } get area() { return this.sideLength ** 2; // ~~~~~~~~~~~~~~~ // error! Object is possibly 'undefined'. } } ``` 如果在其他函数中初始化,则 TS 不能自动识别,需要用 `!:` 显式申明类型: ```typescript 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; } } ``` ### 短路赋值语法 针对以下三种短路语法提供了快捷赋值语法: ```typescript a &&= b; // a && (a = b) a ||= b; // a || (a = b) a ??= b; // a ?? (a = b) ``` ### catch error unknown 类型 Typescript 4.0 之后,我们可以将 catch error 定义为 `unknown` 类型,以保证后面的代码以健壮的类型判断方式书写: ```typescript 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 工厂函数: ```json { "compilerOptions": { "target": "esnext", "module": "commonjs", "jsx": "react", "jsxFactory": "h", "jsxFragmentFactory": "Fragment" } } ``` 还可以通过注释方式覆盖单文件的配置: ```typescript // 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 = ( <>
Hello
); ``` 以上代码编译后解析结果如下: ```typescript // 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 数据流的值,其调用方式是这样的: ```typescript 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); }; ``` `name` 与 `age` 是 Selector 注册的,内部实现方式必然是 `useSelector` + reduce,但类型定义就麻烦了,通过重载可以这么做: ```typescript import * as React from 'react'; import { useSelector } from 'react-redux'; type Function = (...args: any) => any; export function useDesigner(); export function useDesigner( t1: T1 ): ReturnType ; export function useDesigner( t1: T1, t2: T2 ): ReturnType & ReturnType ; export function useDesigner< T1 extends Function, T2 extends Function, T3 extends Function >( t1: T1, t2: T2, t3: T3, t4: T4, ): ReturnType & ReturnType & ReturnType & ReturnType & ; export function useDesigner< T1 extends Function, T2 extends Function, T3 extends Function, T4 extends Function >( t1: T1, t2: T2, t3: T3, t4: T4 ): ReturnType & ReturnType & ReturnType & ReturnType & ; export function useDesigner(...selectors: any[]) { return useSelector((state) => selectors.reduce((selected, selector) => { return { ...selected, ...selector(state), }; }, {}) ) as any; } ``` 可以看到,笔者需要将 `useDesigner` 传入的参数通过函数重载方式一一传入,上面的例子只支持到了三个参数,如果传入了第四个参数则函数定义会失效,因此业界做法一般是定义十几个重载,这样会导致函数定义非常冗长。 但参考 TS4 的例子,我们可以避免类型重载,而通过枚举的方式支持: ```typescript type Func = (state?: any) => any; type Arr = readonly Func[]; const useDesigner = ( ...selectors: T ): ReturnType & ReturnType & ReturnType & ReturnType => { 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](https://github.com/dt-fe/weekly/issues/259) **如果你想参与讨论,请 [点击这里](https://github.com/dt-fe/weekly),每周都有新的主题,周末或周一发布。前端精读 - 帮你筛选靠谱的内容。** > 关注 **前端精读微信公众号** > 版权声明:自由转载-非商用-非衍生-保持署名([创意共享 3.0 许可证](https://creativecommons.org/licenses/by-nc-nd/3.0/deed.zh))