德胜云资讯,添加一些关于程序相关的内容,仅供大家学习交流(https://www.wxclwl.com)

网站地图

搜索
德胜云咨询
前端分类 javascript CSS 正则表达式 html 前端框架 typescript Ajax
热门标签:
最新标签:

typescript concat精读《Typescript 4》typescript中文网万万没想到,

日期:2023/03/27 18:34作者:郑丽青人气:

导读:// This doesn't work because we're feeding in the wrong type for...

作者:黄子毅

转发链接:

https://mp.weixin.qq.com/s/I56QvJSJJsGvPDiuvAHj4g

1 引言

随着 Typescript 4 Beta 的发布《TypeScript 4.0 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<AB>(arr1[A, B]arr2[]): [A, B]; function concat<ABC>(arr1[A, B, C]arr2[]): [A, B, C]; function concat<ABCD>(arr1[A, B, C, D]arr2[]): [A, B, C, D]; function concat<ABCDE>(arr1[A, B, C, D, E]arr2[]): [A, B, C, D, E]; function concat<ABCDEF>(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<A1A2>(arr1[A1]arr2[A2]): [A1, A2]; function concat<A1B1A2>(arr1[A1, B1]arr2[A2]): [A1, B1, A2]; function concat<A1B1C1A2>(  arr1[A1, B1, C1],   arr2[A2]): [A1, B1, C1, A2]; function concat<A1B1C1D1A2>(  arr1[A1, B1, C1, D1],   arr2[A2]): [A1, B1, C1, D1, A2]; function concat<A1B1C1D1E1A2>(  arr1[A1, B1, C1, D1, E1],   arr2[A2]): [A1, B1, C1, D1, E1, A2]; function concat<A1B1C1D1E1F1A2>(  arr1[A1, B1, C1, D1, E1, F1],   arr2[A2]): [A1, B1, C1, D1, E1, F1, A2];

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

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

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

type Arr = readonly any[]; function concat<T extends ArrU 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 = [1234as 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 = [stringstring]; type Numbers = [numbernumber]; // [string, string, number, number] type StrStrNumNum = [...Strings, ...Numbers];

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

A rest element must be last in a tuple type.

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

type Strings = [stringstring]; 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 ArrU extends ArrR>(  f: (...args: [...T, ...U]) => R,   ...headArgs: T{   return (...b: U) => f(...headArgs, ...b); }

测试效果如下:

const foo = (x: string, y: number, z: boolean) => {}; // This doesnt work because were 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 doesnt work because were passing in too many arguments. const f2 = partialCall(foo, "hello"100true"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(123true); // 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: [stringnumber]): void {   // ... } function foo(arg0: string, arg1: number): void {   // ... }

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

type Range = [startnumberendnumber];

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

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!     // Weve 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 FunctionT2 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知识点文章

Typescript 使用日志

想去力扣当前端,TypeScript 需要掌握到什么程度?

TypeScript 4.0 Beta 版本正式发布

「干货」将数十万行CoffeeScript代码迁移到TypeScript

TypeScript中的类型断言详解

深入浅出TypeScript在Model中的高级应用

让人眼前一亮的 10 大 TypeScript 项目

拿6个案例讲解TypeScript 知识点「干货」

TypeScript 中的顶级类型:any 和 unknown

1500行TypeScript代码在React中实现组件keep-alive

「TypeScript」详解一个了不起的 tsconfig.json 指南

用TypeScript编写React的优雅实践「干货」

了不起的 TypeScript 入门教程「实践篇」

了不起的 TypeScript 入门教程「基础篇」

TypeScript 常见问题整理(60多个)「上」

TypeScript 常见问题整理(60多个)「下」

深入TypeScript难点梳理讲解

Vue3.0之前你必须知道的TypeScript实战技巧

深入TypeScript难点梳理讲解

Vue3.0之前你必须知道的TypeScript实战技巧

你需要的 React + TypeScript 50 条规范和经验

TypeScript详细概括【思维导图】

Vue3.0 尝鲜 Hook TypeScript 取代 Vuex【项目实践】

TypeScript详细概括【思维导图】

「新消息」基于JavaScript/TypeScript 编程环境Deno1.0 即将发布

「干货」一张页面引起的项目架构思考(rax+Typescript+hooks)

深入浅出Vue3 跟着尤雨溪学 TypeScript 之 Ref 【实践】

作者:黄子毅

转发链接:

https://mp.weixin.qq.com/s/I56QvJSJJsGvPDiuvAHj4g

排行

网站地图

Copyright © 2002-2022 香港德胜云网络 版权所有 | 备案号:蜀ICP备2023007363号-5

声明: 本站内容全部来自互联网,非盈利性网站仅供学习交流