新增Awaited类型Awaited可以提取Promise的实际返回类型。根据名字可以理解为:等待Promiseresolve后获取的类型。下面是官方文档提供的demo://A=stringtypeA=Awaited>;//B=numbertypeB=Awaited>>;//C=boolean|numbertypeC=Awaited<布尔|承诺<数字>>;捆绑的domlib类型可以用TS替换。由于开箱即用的特性,所有dom内置类型都被捆绑在一起。比如我们可以直接使用TS内置提供的Document类型。可能有时候你不想随着TS版本升级而升级关联的dom内置类型,所以TS提供了指定domlib类型的解决方案,只需要在package.json中声明@typescript/lib-dom:{"dependencies":{"@typescript/lib-dom":"npm:@types/web"}}这个特性提高了TS的环境兼容性,但一般情况下还是建议开箱即用,省去繁琐的配置和更好地维护项目。模板字符串类型还支持类型缩小导出接口Success{type:`${string}Success`;body:string;}exportinterfaceError{type:`${string}Error`;message:string;}exportfunctionhandler(r:Success|Error){if(r.type==="HttpSuccess"){//'r'的类型为'Success'lettoken=r.body;}}模板字符串类型早就支持了,只是现在才支持根据分支条件中的模板字符串来缩小类型。添加新的--modulees2022虽然你可以使用--moduleesnext来保持最新的特性,但是如果你想使用一个稳定的版本号并且支持顶级的await特性,你可以使用es2022。尾递归优化TS类型系统支持尾递归优化。为了便于理解,举下面的例子:typeTrimLeft=Textends`${inferRest}`?TrimLeft:T;//错误:类型实例化过深,可能是无限的。typeTest=TrimLeft<"oops">;尾递归优化前,TS会因为栈太深而报错,但现在可以正确返回执行结果,因为尾递归优化后,不会形成逐渐加深的调用,而是执行完立即退出当前函数,并且堆叠数量将始终保持不变。JS目前还没有实现自动尾递归优化,但是可以通过自定义函数TCO来模拟。下面发布该函数的实现:functiontco(f){varvalue;varactive=false;var累积=[];返回函数accumulator(...rest){accumulated.push(rest);如果(!active){active=true;while(accumulated.length){value=f.apply(this,accumulated.shift());}活动=假;返回值;}};}核心是把递归变成while循环,这样就不会产生栈了。强制保留importTS会在编译过程中杀死未使用的导入,但这次提供了--preserveValueImports参数来禁用此功能,因为以下情况会导致误删除导入:import{Animal}from"./animal.js";eval("console.log(newAnimal().isDangerous())");因为TS无法区分eval中的引用,类似vue的setup语法:支持变量导入类型之前支持以下语法标签声明引用的变量是一个类型:importtype{BaseType}from"./some-module.js";现在支持变量级类型声明:import{someFunc,typeBaseType}from"./some-module.js";构建独立模块时安全擦除BaseType比较方便,因为构建单个模块时无法感知some-module.js文件的内容,所以如果不指定类型BaseType,TS编译器将不会识别为类型变量。类私有变量检查包括两个特性。一是TS支持类私有变量的检查:classPerson{#name:string;}二是支持obj中#name的判断,如:classPerson{#name:string;构造函数(名称:字符串){this.#name=name;}equals(other:unknown){returnother&&typeofother==="object"&nameinother&&//<-这是新的!这个.#name===other.#name;}}这个判断隐含的要求#nameinother中的other是Person实例化的对象,因为这个语法只能存在于类中,可以进一步缩小类型到Person类。导入断言支持导入断言提议:importobjfrom"./something.json"assert{type:"json"};以及动态导入的断言:constobj=awaitimport("./something.json",{assert:{type:"json"}})TS这个特性支持任何类型的断言,不管浏览器是否识别它或不是。因此,断言若要生效,需要以下两种支持中的任意一种:浏览器支持。构建脚本支持。但是,目前构建脚本支持的语法并不统一。例如,Vite有以下两种断言导入类型的方式:importobjfrom"./something?raw"//或者自创语法blob加载方式constmodules=import.meta.glob('./**/index.tsx',{assert:{type:'raw'},},);所以这个importassertion至少可以在以后统一buildtool的语法,甚至让浏览器有了native的支持,就不需要buildtools来处理importassertions了。其实要完全依赖浏览器解析还有很长的路要走,因为一个复杂的前端项目至少有3000~5000个资源文件,不可能在目前的生产环境,因为速度太慢了。constread-onlyassertionconstobj={a:1}asconstobj.a=2//error通过这个语法指定对象的所有属性都是只读的。使用realpathSync.native来实现更快的加载速度是开发者没有察觉到的。就是利用realpathSync.native来提高TS的加载速度。Fragment自动补全增强Class成员函数和JSX属性的自动补全功能得到了增强。使用最新版TS后,应该已经有了动感。比如JSX写标签输入输入后,会根据类型自动补全内容,如://↑回车↓////↑光标自动移动到这里代码可以写在super()之前。JS对super()的限制是之前不能调用这个,但是TS限制比较多。任何在super()之前写的代码都会报错,这显然过于严格了。现在TS放宽了校验策略,只有在super()之前调用这个才会报错,允许执行其他代码。其实这一点早就该改了。如此严格的验证策略让我觉得JS不允许在super()之前调用任何函数,但是想想也不合理,因为super()就是调用父类的构造函数,之所以super()不会自动调用,需要手动调用是为了让开发者可以灵活决定在父类构造函数之前执行哪些逻辑,所以之前TS的一刀切的行为实际上导致super()丢失它存在的意义并成为无意义的样板代码。类型缩小也会对解构产生影响。这个特性真的很强大,就是类型收窄在解构之后仍然生效。之前TS的类型缩小已经很强大了,可以做如下判断:functionfoo(bar:Bar){if(bar.a==='1'){bar.b//string类型}else{bar.b//numbertype}}但是如果a和b是提前从bar中解构出来的,是不能自动变窄的。现在这个问题也得到解决,下面的代码可以工作:functionfoo(bar:Bar){const{a,b}=barif(a==='1'){b//stringtype}else{b//numbertype}}深度递归类型检查优化下面的赋值语句会因为属性prop的类型不匹配而产生异常:interfaceSource{prop:string;}interfaceTarget{prop:number;}functioncheck(source:Source,target:目标){target=来源;//错误!//类型“Source”不可分配给类型“Target”。//属性'prop'的类型不兼容。//类型'string'不能分配给类型'number'。}这很容易理解。从报错来看,TS也会根据递归检测的方式发现prop类型不匹配。但是由于TS支持泛型,下面是一个无限递归的例子:}functioncheck(source:Source,target:Target){target=source;}其实不需要像官方描述的那么复杂,props:Source就足够了使示例无限递归下去。为了保证这种情况不出错,TS做了递归的深度判断。太深的递归会终止判断,但这会带来一个问题,就是无法识别如下错误:interfaceFoo{prop:T;}declareletx:Foo>>>>>;声明让y:Foo>>>>;x=y;为了解决这个问题TS做了一个判断:递归保护只对递归写的场景有效,而上面的例子,虽然也是很深层次的递归,但是因为是人写的,TS会拿麻烦一一写了。递归地使场景正常工作。这个优化的核心是TS可以根据代码结构分析出哪些递归是“非常抽象/启发式”的写法导致的,哪些是枚举引起的递归,免除后者的递归深度检查。增强的索引推导下面官方文档给出的例子乍一看很复杂。我们拆解分析一下:interfaceTypeMap{"number":number;“字符串”:字符串;"boolean":boolean;}typeUnionRecord={[KinP]:{种类:K;v:TypeMap[K];f:(p:TypeMap[K])=>void;}}[P];functionprocessRecord(record:UnionRecord){record.f(record.v);}//这个调用曾经有问题-现在可以了!processRecord({kind:"string",v:"hello!",//'val'曾经隐式具有'string|number|boolean'类型,//但现在被正确地推断为'string'。f:val=>{console.log(val.toUpperCase());}})本例的目的是实现processRecord函数,通过识别传入参数kind,自动推导出回调函数f中value的类型。比如kind:"string",那么val就是string类型,如果kind:"number",那么val就是numeric类型。因为这次TS的更新解决了之前不能识别val类型的问题,所以我们不用关心TS是怎么解决的,只要记住TS能正确识别场景即可(有点像围棋的set公式,对于经典例子最好一个一个学习),并了解场景是如何构建的。怎么做?首先定义一个类型映射:interfaceTypeMap{"number":number;“字符串”:字符串;"boolean":boolean;}然后定义最后一个函数processRecord:functionprocessRecord(record:UnionRecord){record.f(record.v);}这里定义了一个通用的K,KextendskeyofTypeMap等价于Kextends'number'|'字符串'|'boolean',所以这里定义了以下泛型K的取值范围,取值为这三个字符串之一。重点来了,参数记录需要根据传入的种类来判断f回调函数的参数类型。我们先想象一下下面的UnionRecord类型怎么写:typeUnionRecord={kind:K;v:TypeMap[K];f:(p:TypeMap[K])=>void;}如上,一个很自然的想法就是定义一个泛型K,这样kind和f,p类型就可以表示了,这样processRecord的UnionRecord(record:UnionRecord)表示当前收到的实际类型K被传递到UnionRecord中,以便UnionRecord知道实际处理什么类型。这个函数到这里就结束了,但是UnionRecord的官方定义略有不同:typeUnionRecord={[KinP]:{kind:K;v:TypeMap[K];f:(p:TypeMap[K])=>void;}}[P];这个例子故意增加了复杂度,使用了索引的方式来绕过去。可能之前TS无法解析这个表单。反正现在也支持这种写法。我们来看看为什么这个写法和上面的是等价的。上面的写法简化如下:typeUnionRecord={[KinP]:X}[P];可以理解为UnionRecord定义了一个GenericP,这个函数根据索引(或者理解为下标)[P]从对象{[KinP]:X}中获取类型。而[KinP],描述对象Key值的类型定义,相当于定义了多个类型。由于PextendskeyofTypeMap,所以可以这样理解类型展开:typeUnionRecord={'number':X,'string':X,'boolean':X}[P];而P是泛型,因为[KinP]的定义,它肯定能命中上面的其中一项,所以其实相当于下面的简单写法:typeUnionRecord={种类:K;v:TypeMap[K];f:(p:TypeMap[K])=>void;}parametercontrol流分析这个特性直译比较奇怪,我们从代码上理解:typeFunc=(...args:["a",数字]|["b",字符串])=>void;constf1:Func=(kind,payload)=>{if(kind==="a"){payload.toFixed();//'payload'缩小为'number'}if(kind==="b"){payload.toUpperCase();//'payload'缩小为'string'}};f1("a",42);f1("b","hello");如果参数定义为数组并使用或并行枚举,实际上可能包含运行时类型缩小。例如,当第一个参数的值为a时,则确定第二个参数的类型为number;当第一个参数的值为b时,判断第二个参数的类型为string。值得注意的是,这种推导是从前向后推导的,因为参数是从左向右传递的,所以前面推导后面,而不是后面推导前面(比如不能理解为第二个)参数为数字类型,第一个参数的值必须为a)。移除JSX编译过程中产生的非必要代码。编译JSX时,去掉最后一个无意义的void0,减少代码量:-exportconstel=_jsx("div",{children:"foo"},void0);+exportconstel=_jsx("div",{孩子们:"foo"});由于改动不大,大家可以借此机会了解一下TS源码是如何修改的。这是PRDIFF地址。可以看到,修改位置是src/compiler/transformers/jsx.ts文件,修改逻辑是去掉factory.createVoidZero()函数。顾名思义,这个函数最后会创建void0,还有很多其他的tests文件的修改,其实了解源码上下文,这个修改并不难。JSDoc验证提示JSDoc注释与代码分离,在不断迭代的过程中很容易与实际代码进行分叉:/***@paramx{number}第一个操作数*@paramy{number}第二个操作数*/functionadd(a,b){returna+b;}现在TS可以提示命名、类型等不一致,顺便说一句,如果你用TS,尽量不要用JSDoc,毕竟有一个随时存在代码和类型分离不一致的风险。总结从这两次更新来看,TS已经进入了成熟阶段,但是TS在泛型类的问题上还处于早期阶段。有大量的复杂场景无法支持,或者没有优雅的兼容方案。希望以后能不断完善对复杂场景的类型支持。讨论地址为:Jingdu《Typescript 4.5-4.6 新特性》·Issue#408·dt-fe/weekly想参与讨论的请点这里,每周都有新话题,周末或周一发布。前端精读——帮你过滤靠谱的内容。关注前端精读微信公众号版权声明:免费转载-非商业-非衍生保留属性(CreativeCommons3.0License)