158. 精读《Typescript 4》
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 T
与 U
是数组类型,再通过 [...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// vsideLength!: number;// ^^^^^^^^// type annotationconstructor(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);
};
name
与 age
是 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 不会对项目带来实质影响,期待正式版的发布。
更多讨论
如果你想参与讨论,请关注前端精读 周刊- 每周一帮你筛选靠谱的内容。
以下是我个人微信,欢迎交流(加好友注明来自前端精读)!
(如想获得阿里内推机会,发简历给我,邮箱地址:
ziyi.hzy@alibaba-inc.com
PS:招人阶段,欢迎大家砸简历~)
158. 精读《Typescript 4》相关推荐
- socket可以写成单例嘛_精读《设计模式 - Singleton 单例模式》
Singleton(单例模式) Singleton(单例模式)属于创建型模式,提供一种对象获取方式,保证在一定范围内是唯一的. 意图:保证一个类仅有一个实例,并提供一个访问它的全局访问点. 其实单例模 ...
- 精读《REST,GraphQL,Webhooks gRPC 如何选型》
1 引言 每当项目进入联调阶段,或者提前约定接口时,前后端就会聚在一起热火朝天的讨论起来.可能 99% 的场景都在约定 Http 接口,讨论 URL 是什么,入参是什么,出参是什么. 有的团队前后端接 ...
- socket可以写成单例嘛_精读设计模式 Singleton 单例模式
Singleton(单例模式) Singleton(单例模式)属于创建型模式,提供一种对象获取方式,保证在一定范围内是唯一的. 意图:保证一个类仅有一个实例,并提供一个访问它的全局访问点. 其实单例模 ...
- 精读《如何编译前端项目与组件》
1 引言 说到前端编译方案,也就是如何打包项目,如何编译组件,可选方案有很多,比如: 通过 webpack / parcel / gulp 构建项目. 通过 parcel / gulp / babel ...
- typescript类与继承
1 /* 2 1.vscode配置自动编译 3 4 1.第一步 tsc --inti 生成tsconfig.json 改 "outDir": "./js", 5 ...
- 18日精读掌握《费曼物理学讲义-卷一》计划(2019/6/12-2019/6/29)
本篇目录 18日精读掌握<费曼物理学讲义-卷一>计划概览 作者介绍: 本书内容介绍: 本书目录 书本封面 18日精读掌握<费曼物理学讲义-卷一>计划概览 我将在接下来的18天掌 ...
- 精读《设计模式 - Observer 观察者模式》
Observer(观察者模式) Observer(观察者模式)属于行为型模式. 意图:定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并被自动更新. 拿项目的 ...
- TypeScript看完就会了
TypeScript 什么是TypeScript? TypeScript是由微软开发的一款开源的编程语言 TypeScript是JavaScript的超集,遵循最新的ES5 ES6规范,TypeScr ...
- 精读《Spring 概念》
spring 是 Java 非常重要的框架,且蕴含了一系列设计模式,非常值得研究,本期就通过 Spring学习 这篇文章了解一下 spring. spring 为何长寿 spring 作为一个后端框架 ...
最新文章
- Conversion error:Jekyll::Converters::Scss encountered an error while converting css/main.scss
- windows系统杀掉explorer.exe进程后黑屏
- 微软为华为定制了一个“烂笔头小冰”,让人想起了老罗的“闪念胶囊”
- mysql一列的第二个值,mysql – 如果另一列有多个值,如何选择按列分组的值
- 获取http请求标头_HTTP请求和标头参数的CDI拦截器–简单示例
- Linux下python升级步骤
- 计数问题(洛谷-P1980)
- aspx后台调用前台jquery_jQuery调用Asp.Net后台方法
- Spark中RDD与DataFrame与DataSet的区别与联系
- 程序读取计算机设备管理器中各设备的状态(启用/禁用)?(转自大富翁)
- BigBrother服务器端管理脚本_Bash
- Python入门基础学习记录(二)汇率案例学习记录
- 一名 IT 工程师的九年工作总结!
- PC微信逆向之发送消息
- ips 代理模式_IPS的完整形式是什么?
- 初识html5-当当网图书分类页面,html+css静态页面当当网案例
- 超实用!常用贴片三极管丝印与型号对照表
- 对于路由地址并未切换,但是地址栏发生地址发生变化原因
- 新点软件怎么导入清单_新点软件怎么导入excel清单表格 表格有什么要求???...
- 揭秘!谷歌云确立领先地位的五大变革
热门文章
- arduino 红外遥控小车
- 荣耀linux改装win10教程,华为笔记本linux改win10教程|华为笔记本重装win10
- ipqc异常处理流程图_IPQC工作流程图
- 基于微信小程序的西餐外卖系统的设计与实现NodeJS-计算机毕业设计
- 一行Python代码去除照片背景
- c语言-结构体实例笔记
- 数据可视化实验:python数据可视化-柱状图,条形图,直方图,饼图,棒图,散点图,气泡图,雷达图,箱线图,折线图
- Shell脚本模拟用户行为刷App积分,学习娱乐之用,再加图像数字识别验证码登录
- NFS服务配置与mount nfs时-o nolock的问题
- React-Redux 中文文档