当前期刊数: 158
1 引言 随着 Typescript 4 Beta 的发布,又带来了许多新功能,其中 Variadic Tuple Types 解决了大量重载模版代码的顽疾,使得这次更新非常有意义。
2 简介 可变元组类型 考虑 concat
场景,接收两个数组或者元组类型,组成一个新数组:
1 2 3 function concat (arr1, arr2 ) { return [...arr1, ...arr2]; }
如果要定义 concat
的类型,以往我们会通过枚举的方式,先枚举第一个参数数组中的每一项:
1 2 3 4 5 6 7 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 次重载,代码几乎不可维护:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 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 ];
如果我们采用批量定义的方式,问题也不会得到解决,因为参数类型的顺序得不到保证:
1 function concat<T, U>(arr1 : T[], arr2, U[]): Array <T | U>;
在 Typescript 4,可以在定义中对数组进行解构,通过几行代码优雅的解决可能要重载几百次的场景:
1 2 3 4 5 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
,返回除第一项外剩下元素:
1 2 3 4 function tail (arg ) { const [_, ...result] = arg; return result; }
同样告诉 TS T
是数组类型,且 arr: readonly [any, ...T]
申明了 T
类型表示除第一项其余项的类型,TS 可自动将 T
类型关联到对象 rest
:
1 2 3 4 5 6 7 8 9 10 11 12 13 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" ];const r1 = tail (myTuple);const r2 = tail ([...myTuple, ...myArray] as const );
另外之前版本的 TS 只能将类型解构放在最后一个位置:
1 2 3 4 5 type Strings = [string , string ];type Numbers = [number , number ];type StrStrNumNum = [...Strings , ...Numbers ];
如果你尝试将 [...Strings, ...Numbers]
这种写法,将会得到一个错误提示:
1 A rest element must be last in a tuple type.
但在 Typescript 4 版本支持了这种语法:
1 2 3 4 5 type Strings = [string , string ];type Numbers = number [];type Unbounded = [...Strings , ...Numbers , boolean ];
对于再复杂一些的场景,例如高阶函数 partialCall
,支持一定程度的柯里化:
1 2 3 function partialCall (f, ...headArgs ) { return (...tailArgs ) => f (...headArgs, ...tailArgs); }
我们可以通过上面的特性对其进行类型定义,将函数 f
第一个参数类型定义为有顺序的 [...T, ...U]
:
1 2 3 4 5 6 7 8 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); }
测试效果如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 const foo = (x: string , y: number , z: boolean ) => {};const f1 = partialCall (foo, 100 );const f2 = partialCall (foo, "hello" , 100 , true , "oops" );const f3 = partialCall (foo, "hello" );f3 (123 , true ); f3 ();f3 (123 , "hello" );
值得注意的是,const f3 = partialCall(foo, "hello");
这段代码由于还没有执行到 foo
,因此只匹配了第一个 x:string
类型,虽然后面 y: number, z: boolean
也是必选,但因为 foo
函数还未执行,此时只是参数收集阶段,因此不会报错,等到 f3(123, true)
执行时就会校验必选参数了,因此 f3()
时才会提示参数数量不正确。
元组标记 下面两个函数定义在功能上是一样的:
1 2 3 4 5 6 7 function foo (...args: [string , number ] ): void { }function foo (arg0: string , arg1: number ): void { }
但还是有微妙的区别,下面的函数对每个参数都有名称标记,但上面通过解构定义的类型则没有,针对这种情况,Typescript 4 支持了元组标记:
1 type Range = [start : number , end : number ];
同时也支持与解构一起使用:
1 type Foo = [first : number , second?: string , ...rest : any []];
Class 从构造函数推断成员变量类型 构造函数在类实例化时负责一些初始化工作,比如为成员变量赋值,在 Typescript 4,在构造函数里对成员变量的赋值可以直接为成员变量推导类型:
1 2 3 4 5 6 7 8 9 10 11 class Square { area; sideLength; constructor (sideLength: number ) { this .sideLength = sideLength; this .area = sideLength ** 2 ; } }
如果对成员变量赋值包含在条件语句中,还能识别出存在 undefined
的风险:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 class Square { sideLength; constructor (sideLength: number ) { if (Math .random ()) { this .sideLength = sideLength; } } get area () { return this .sideLength ** 2 ; } }
如果在其他函数中初始化,则 TS 不能自动识别,需要用 !:
显式申明类型:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 class Square { sideLength!: number ; constructor (sideLength: number ) { this .initialize (sideLength); } initialize (sideLength: number ) { this .sideLength = sideLength; } get area () { return this .sideLength ** 2 ; } }
短路赋值语法 针对以下三种短路语法提供了快捷赋值语法:
1 2 3 a &&= b; a ||= b; a ??= b;
catch error unknown 类型 Typescript 4.0 之后,我们可以将 catch error 定义为 unknown
类型,以保证后面的代码以健壮的类型判断方式书写:
1 2 3 4 5 6 7 8 9 10 11 12 13 try { } catch (e) { console .log (e.toUpperCase ()); if (typeof e === "string" ) { console .log (e.toUpperCase ()); } }
PS:在之前的版本,catch (e: unknown)
会报错,提示无法为 error
定义 unknown
类型。
自定义 JSX 工厂 TS 4 支持了 jsxFragmentFactory
参数定义 Fragment 工厂函数:
1 2 3 4 5 6 7 8 9 { "compilerOptions" : { "target" : "esnext" , "module" : "commonjs" , "jsx" : "react" , "jsxFactory" : "h" , "jsxFragmentFactory" : "Fragment" } }
还可以通过注释方式覆盖单文件的配置:
1 2 3 4 5 6 7 8 9 10 11 12 import { h, Fragment } from "preact" ;let stuff = ( <> <div > Hello</div > </> );
以上代码编译后解析结果如下:
1 2 3 4 5 6 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 数据流的值,其调用方式是这样的:
1 2 3 4 5 6 7 8 9 10 11 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,但类型定义就麻烦了,通过重载可以这么做:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 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 的例子,我们可以避免类型重载,而通过枚举的方式支持:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 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
如果你想参与讨论,请 点击这里 ,每周都有新的主题,周末或周一发布。前端精读 - 帮你筛选靠谱的内容。
关注 前端精读微信公众号
版权声明:自由转载-非商用-非衍生-保持署名(创意共享 3.0 许可证 )