后台TypeScript3.8带来了一个新特性:只是导入/导出声明。在上一篇文章中,我们利用这个特性解决了导入类型文件时报错的问题。其实这个特性并不复杂,只是我们需要了解它背后的机制,了解Babel和TypeScript是如何协同工作的。本文主要内容:什么是“import/exportdeclarationonly”Babel和TypeScript如何协同工作的文字首先,我们先来介绍一下这个特性。什么是“justimport/exportdeclarations”为了允许我们导入类型,TypeScript重用了JavaScript导入语法。例如,在下面的示例中,我们确保JavaScript值doThing与TypeScript类型Options一起导入://./foo.tsinterfaceOptions{//...}exportfunctiondoThing(options:Options){//...}//./bar.tsimport{doThing,Options}from'./foo.js';functiondoThingBetter(options:Options){//做一些好的事doThing(options);doThing(options);}这很方便,因为在大多数情况下,我们不必担心要导入什么——只要我们想导入什么。不幸的是,这只适用于称为“导入省略”的功能。当TypeScript输出一个JavaScript文件时,TypeScript会识别Options只是作为一种类型使用,它会删除Options。//./foo.jsexportfunctiondoThing(options:Options){//...}//./bar.jsimport{doThing}from'./foo.js';functiondoThingBetter(options:Options){//做一些好的事情doThing(options);doThing(options);}通常情况下,这种行为更好。但它会导致其他一些问题。首先,在某些场景下,TypeScript会混淆导出的是类型还是值。例如,在下面的示例中,MyThing是值还是类型?从'./some-module.js'导入{MyThing};导出{MyThing};如果单看这个文件,我们无从知晓答案。如果Mything只是一个类型,Babel和TypeScript使用的transpileModuleAPI编译的代码将无法正常工作,TypeScript的isolatedModules编译选项会提示我们,这种写法会抛出错误。问题的关键在于没有办法识别它只是一种类型,以及是否应该删除它,所以“导入省略”还不够好。此外,还有另一个问题,TypeScriptimportelision将去除仅包含类型声明的导入语句。这导致具有副作用的模块的行为明显不同。因此,用户将不得不添加一个额外的语句来确保副作用。//由于importelision.import{SomeTypeFoo,SomeOtherTypeBar}from'./module-with-side-effects',这条语句会被删除;//Thisstatementalwayssticksaround.import'./module-with-side-effects';我们在Angularjs(1.x)中看到的一个具体示例是服务需要全局注册(这是一个副作用),但导入的服务仅用于类型声明。//./service.tsexport类服务{//...}register('globalServiceId',Service);//./consumer.tsimport{Service}from'./service.js';inject('globalServiceId',function(service:Service){//用Service做事});导致./service.js中的代码不会被执行,导致运行时中断。在TypeScript3.8中,我们添加了import/export-only声明语法作为解决方法。importtype{SomeThing}from"./some-module.js";exporttype{SomeThing};importtype仅导入用于类型注释或声明的声明,这些声明始终被完全删除,因此在运行时将无代码被留下来。同样,exporttype只为类型提供一个导出,在TypeScript输出文件中也会被移除。值得注意的是,类在运行时有值,在设计时有类型。它的使用取决于上下文。当使用导入类型导入一个类时,你不能做类似从它继承的事情。importtype{Component}from"react";interfaceButtonProps{//...}classButtonextendsComponent{//~~~~~~~~~//错误!'Component'仅指type,但在这里用作值。//...}如果你以前使用过Flow,它们的语法是相似的。一个区别是我们添加了一个新的约束以避免可能混淆的代码。//只有'Foo'是一个类型吗?或者导入中的每个声明?//我们只是给出一个错误,因为它不是clear.importtypeFoo,{Bar,Baz}from"some-module";//~~~~~~~~~~~~~~~~~~~~~//错误!仅类型导入可以指定默认导入或命名绑定,但不能同时指定两者。与导入类型相关联,我们提供了一个新的编译选项:importsNotUsedAsValues,可用于控制如何处理未使用的导入语句。它的名称是暂定的,但它提供了三个不同的选项。删除,这是当前的行为-丢弃这些导入语句。这仍然是默认行为,没有要保留的重大更改,它会保留所有语句,即使它们从未使用过。它可以保留副作用。错误,这将保留所有导入(与保留选项相同)语句,但当仅为类型导入值时会抛出错误。如果您想确保不会意外导入任何值,这会很有用,但对于副作用,您仍然需要添加额外的导入语法。有关此功能的更多信息,请参阅此PR。Babel和TypeScript如何协同工作TypeScript做了两件事来为JavaScript代码添加静态类型检查。将TS+JS代码转换成各种JS版本。Babel还做了第二件事。Babel的做法(尤其是transform-typescript插件)是:先去掉类型,再做转换。这样,您可以使用Babel的所有好处,同时仍然能够提供ts文件。看一个例子:在babel编译之前://example.tsimport{Color}from"./types";constchangeColor=(color:Color)=>{window.color=color;};babel编译后://示例。jsconstchangeColor=(color)=>{window.color=color;};在这里,babel无法告诉example.tsColor实际上是一个类型。因此,babel也被迫错误地将此声明保留在转译后的代码中。为什么?Babel在转译期间显式地一次处理一个文件。大概是因为babel团队不想构建与TypeScript相同的类型解析过程,只是为了删除类型。isolatedModulesisolatedModules是一个TypeScript编译器选项,旨在充当保护措施。tsc在做类型检查的时候,当检测到isolatedModules被启用时,就会报类型错误。如果错误没有解决,会影响独立处理文件的编译工具(babel)。来自TypeScript文档:执行额外的检查以确保单独的编译(例如使用transpileModule或@babel/plugin-transform-typescript)是安全的。来自Babel文档:--isolatedModules这是默认的Babel行为,它不能被关闭,因为Babel不支持跨文件分析。也就是说,每个ts文件都必须能够独立编译。isolatedModules标志防止我们引入模棱两可的导入。下面看两个具体的例子,看几个例子,理解isolatedModules标签的重要性。1.MixinImport,MixinExport这里我们获取types.ts文件中定义的类型,并从中重新导出它们。当isolatedModules打开时,这段代码将不会通过类型检查。//types.tsexporttypePlaylist={id:string;名称:字符串;trackIds:字符串[];};导出类型Track={id:string;名称:字符串;艺术家:字符串;duration:number;};//lib-ambiguous-re-export.tsexport{Playlist,Track}from"./types";从“./api”导出{CreatePlaylistRequestParams,createPlaylist};Babel转译://dist/types.js--empty--//dist/lib-ambiguous-re-export.jsexport{Playlist,Track}from"./types";从“./api”导出{CreatePlaylistRequestParams,createPlaylist};error:someunderstanding:Babelfromourtypesmodule删除所有内容后,它只包含类型。Babel没有对我们的lib模块进行任何转译。Playlist和Track应该被Babel移除。从Node的角度来看,Node在做模块分析的时候,会发现types.js中引入的文件是空的,报错:文件不存在。如屏幕截图所示,tsc类型检查过程立即将这些不明确的重新导出报告为错误。2.显式类型导入、显式类型导出这一次,我们显式地重新导出lib-import-export.ts中的类型。当打开isolatedModules时,此代码将通过tsc类型检查。编译前://types.tsexporttypePlaylist={id:string;名称:字符串;trackIds:字符串[];};//lib-import-export.tsimport{PlaylistasPlaylistType,TrackasTrackType,}from"./types";import{CreatePlaylistRequestParamsasCreatePlaylistRequestParamsType,createPlaylist}from"./api";exporttypePlaylist=PlaylistType;exporttypeTrack=TrackType;导出类型CreatePlaylistRequestParams=CreatePlaylistRequestParamsType;export{createPlaylist};编译后://dist/types.js--empty--TODO还是babel将其全部删除?//lib-import-export.jsimport{createPlaylist}from"./api";导出{创建播放列表};此时:Babel仍然输出一个空类型。.js文件。但这没关系,因为我们编译的lib-import-export.js不再引用它。TypeScript3.8如前所述,TypeScript3.8引入了一种新语法——“仅导入/导出声明”。此语法在使用时为类型解析过程增加了确定性。现在,编译器(无论是tsc、babel还是其他)将能够查看单个文件,如果它是TypeScript类型,则取消导入或导出。importtype...from—让编译器知道您导入的绝对是一种类型。exporttype...from—相同,仅用于导出。//src/lib-type-re-export.tsexporttype{Track,Playlist}from"./types";从“./api”导出类型{CreatePlaylistRequestParams};export{createPlaylist}from"./api";//将被编译为://dist/lib-type-re-export.jsexport{createPlaylist}from"./api";更多参考TS文档的新部分:https://www.typescriptlang.org/docs/handbook/modules.html#importing-types介绍了用于类型导入的TSPR。PR说明中有很多重要信息:https://github.com/microsoft/TypeScript/pull/35200TS3.8公告:https://devblogs.microsoft.com/typescript/announcing-typescript-3-8-beta/#type-only-imports-exportsBabelPR,增强了babel解析器和transform-typescript插件以利用新语法。与Babel7.9一起发布:https://github.com/babel/babel/pull/11171