今天,我们很兴奋地发布TypeScript编程语言的下一个重要里程碑的测试版:TypeScript4.0。
这个测试版让我们迈出了进入TypeScript4.0的第一步,虽然它带来了一个新的主要版本,但不要担心-没有比往常更大的突破性变化。我们发展TypeScript的理念一直是提供一条升级路径,将破坏性的破坏性更改降至最低,同时仍给我们一些灵活性,以便在适当的时候将可疑代码标记为错误。出于这个原因,我们继续使用与以前版本类似的版本控制模型,因此4.0是TypeScript3.9的自然延续。
要开始使用测试版,您可以通过NuGet获取它,或者通过以下命令使用NPM:
现在让我们来看看TypeScript4.0将会有什么!
考虑JavaScript中一个名为CONCAT的函数,该函数接受两个数组或元组类型,并将它们连接在一起作为一个新的数组。
还要考虑Tail,它接受一个数组或元组,并返回除第一个元素之外的所有元素。
对于Concat,我们在该语言的旧版本中所能做的唯一有效的事情就是尝试并编写一些重载。
函数拼接<;>;(arr1:[],arr2:[]):[a];函数拼接<;A>;(arr1:[a],arr2:[]):[a];函数拼接<;A,B,C>;(arr1:[A,B],arr2:[]):[a,B];函数拼接<;A,B,C>;(arr1:[A,B,C],arr2:[]):[A,B,C];函数拼接<;A,B,C,D>;(arr1:[A,B,C,D],arr2:[]):[A,B,C,D];函数拼接<;A,B,C,D,E>;(arr1:[A,B,C,D,E],arr2:[]):[a,B,C,D,E];函数串联<;A,B,C,D,E,F>;(arr1:[A,B,C,D,E,F],arr2:[]):[A,B,C,D,E,F];)
呃…。好的,那是…。当第二个数组始终为空时的七个重载。让我们为arr2有一个参数时添加一些。
函数拼接(<;A2&>;(arr1:[],arr2:[A2]):[A2];函数拼接<;A1,A2>;(arr1:[A1],arr2:[A2]):[A1,A2];函数拼接<;A1,B1,A2>;(arr1:[A1,B1],arr2:[A2]):[A1,B1,A2];函数拼接(&L。A1,B1,C1,A2>;(arr1:[A1,B1,C1],arr2:[A2]):[A1,B1,C1,A2];函数拼接<;A1,B1,C1,D1,A2>;(arr1:[A1,B1,C1,D1],arr2:[A2]):[A1,B1,C1,D1,A2];函数拼接(<);A1,B1,C1,D1,E1,A2>;(arr1:[A1,B1,C1,D1,E1],arr2:[A2]):[A1,B1,C1,D1,E1,A2];函数级联;A1,B1,C1,D1,E1,F1,A2>;(arr1:[A1,B1,C1,D1,E1,F1],arr2:[A2]):[A1,B1,C1,D1,E1,F1,A2];
我们希望这一点很清楚,这变得越来越不合理。不幸的是,键入Tail这样的函数也会出现同样的问题。
这是另一个我们喜欢称之为“千载之死”的案例,它甚至不能解决一般的问题。它只为我们愿意编写的重载提供正确的类型。如果我们想要创建一个包罗万象的案例,我们需要一个如下所示的重载:
但是在使用元组时,该签名没有编码任何关于输入长度或元素顺序的内容。
TypeScript4.0带来了两个根本性的变化,以及推理方面的改进,使键入这些内容成为可能。
第一个更改是,元组类型语法中的跨页现在可以是泛型的。这意味着,即使我们不知道正在操作的实际类型,我们也可以表示对元组和数组的高阶操作。在这些元组类型中实例化(或用真实类型替换)泛型分布时,它们可以生成其他数组和元组类型集。
例如,这意味着我们可以像Tail一样输入函数,而不会出现“一千个重载而死”的问题。
函数Tail<;T扩展Any[]>;(arr:readonly[any,.。t]){const[_已忽略,.。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([.。我的元组,..。myArray]as const);
第二个变化是扩散元素可以出现在元组中的任何地方-而不仅仅是末尾!
type Strings=[String,String];type Numbers=[Number,Number];//[String,String,Number,Number]type StrNumNum=[.。弦,..。号码];
当我们在没有已知长度的类型中展开时,结果类型也变得无界,并且所有连续的元素都会成为结果REST元素类型的因子。
type String=[String,String];type Numbers=Number[]//[String,String,.Array<;Number|Boolean&>;]type Unbound=[.。弦,..。数字,布尔值];
通过将这两种行为结合在一起,我们可以为Concat编写单个类型正确的签名:
type arr=readonly any[];函数conat<;T扩展arr,U扩展arr>;(arr1:t,arr2:u):[.。T,..。u]{返回[.。阵列1,.。arr2];}。
虽然这一个签名仍然有点长,但它仍然只是一个签名,它只需要编写一次,而且它实际上在所有数组和元组上都提供了可预测的行为。
此功能本身很棒,但也有其他更复杂的场景。例如,考虑一个部分应用名为artialCall的参数的函数。artialCall接受一个函数以及该函数所需的几个初始参数。然后,它返回一个新函数,该函数接受该函数需要的任何其他参数,并一起调用它们。
TypeScript4.0改进了REST参数和REST元组元素的推理过程,这样我们就可以键入它并使其“正常工作”。
类型arr=只读未知[];函数partalCall<;T扩展arr,U扩展arr,R>;(f:(.。参数:[.。T,..。U])=>;R,.。headArgs:t){return(.。B:u)=>;f(.。HeadArgs,..。b)}。
在本例中,artialCall了解它最初可以接受哪些参数,不可以接受哪些参数,并返回适当接受和拒绝任何剩余内容的函数。
const foo=(x:string,y:number,z:boolean)=>;{}//这不起作用,因为我们输入了错误的';x&39;类型。const F1=partalCall(foo,100);//~//错误!';数字';类型的参数不能赋给';字符串';类型的参数。//这不起作用,因为我们传入了太多参数。const f2=partalCall(foo,";hello";,100,true,";oops";)//~//错误!应为4个参数,但得到了5个参数。//这起作用!它的类型为';(y:number,z:boolean)=>;void';const f3=artialCall(foo,";hello";);//现在我们可以对f3做什么?F3(123,true);//有效!F3();//错误!应为2个参数,但实际为0。F3(123,";hello";);//~//错误!类型为';";hello";';的参数不能赋值给类型为';Boolean';的参数。
可变元组类型支持许多新的令人兴奋的模式,特别是在函数组合方面。我们希望能够利用它来更好地检查JavaScript的内置绑定方法的类型。一些其他推理改进和模式也涉及到这一点,如果您有兴趣了解更多信息,可以查看各种元组的Pull请求。
改善有关元组类型和参数列表的体验非常重要,因为它允许我们围绕常见的JavaScript习惯用法进行强类型验证-实际上只是对参数列表进行切片和切分,然后将它们传递给其他函数。我们可以将元组类型用于REST参数的想法是至关重要的。
例如,以下函数使用元组类型作为REST参数…
不过,有一个地方的差异开始变得明显起来:可读性。在第一个示例中,我们没有第一个和第二个元素的参数名称。虽然这些对类型检查没有影响,但是元组位置上缺少标签会使它们更难使用-更难传达我们的意图。
进一步推动参数列表和元组类型之间的联系,我们已经使REST元素和可选元素的语法与参数列表的语法相对应。
标记元组元素时,也必须标记元组中的所有其他元素。
type Bar=[first:string,number];//~//错误!元组成员必须都有名称,或者都没有名称。
值得注意的是,标签在解构时不要求我们以不同的方式命名变量。它们在那里纯粹是为了文档和工具。
函数foo(x:[First:String,Second:Number]){//.//注意:我们不需要将它们命名为';First';和';Second';let[a,b]=x;//.}。
总体而言,当利用元组和参数列表周围的模式以及以类型安全的方式实现重载时,带标签的元组是很方便的。要了解更多信息,请查看带标签的元组元素的拉取请求。
当启用了noImplitAny时,TypeScript4.0现在可以使用控制流分析来确定类中的属性类型。
类Square{//以前:IMPLICIT ANY!//NOW:推断为`number`!Area;side Length;构造函数(side Length:Number){这个。side Length=side Length;此。面积=侧长**2;}}。
在构造函数并非所有路径都分配给实例成员的情况下,该属性被认为可能是未定义的。
类Square{side Length;构造函数(side Length:Number){IF(Math.。Random()){这个。side Length=side Length;}}get area(){返回此。side Length**2;//~//错误!对象可能是';未定义的';。}}。
在您更了解的情况下(例如,您有某种初始化方法),您将需要一个显式类型注释以及一个明确的赋值断言(!)。如果您在strictPropertyInitialization中。
类Square{//确定赋值断言//v side Length!:number;//^//类型批注构造函数(side Length:number){this。Initialize(Side Length)}Initialize(side Length:number){this。side Length=side Length;}get area(){返回此。侧长**2;}}。
JavaScript和许多其他语言都支持一组称为复合赋值运算符的运算符。复合赋值运算符将运算符应用于两个参数,然后将结果赋给左侧。您可能以前见过这些内容:
//加法//a=a+b a+=b;//减法//a=a-b a-=b;//乘法//a=a*b a*=b;//除法//a=a/b a/=b;//幂//a=a**b a**=b;//左位移位//a=a<;<;b a<;<;=b;
JavaScript中的许多运算符都有对应的赋值运算符!但是有三个值得注意的例外:逻辑与(&;&;)、逻辑或(||)和空值合并(??)。
这就是为什么TypeScript4.0支持一个很有前途的建议,即添加三个新的赋值操作符:&;&;=、||=和??=。
这些运算符非常适合替换用户可能编写如下代码的任何示例:
甚至还有一些我们看到的(或者,呃,我们自己编写的)在需要时懒惰地初始化值的模式。
在极少数情况下,您使用有副作用的getter或setter,值得注意的是,这些操作符仅在必要时执行赋值。从这个意义上说,赋值是短路的,这是它们与其他复合赋值的唯一不同之处。
我们非常感谢社区成员王文禄的贡献!
自从TypeScript问世以来,CATCH子句变量的类型始终是ANY。这意味着打字稿允许你对它们做任何你想做的事情。
尝试{//.}catch(X){//x具有类型';任何';-玩得开心!控制台。对数(x.。消息);控制台。对数(x.。toUpperCase());x++;x。雅达。雅达。yadda();}。
如果我们试图防止错误处理代码中意外发生更多错误,则上面有一些不受欢迎的行为!因为默认情况下这些变量的类型为ANY,所以它们缺乏任何可以防止无效操作的类型安全。
这就是为什么TypeScript4.0现在允许您将CATCH子句变量的类型指定为UNKNOWN的原因。未知比任何类型都安全,因为它提醒我们在对值进行操作之前需要执行某种类型检查。
尝试{//.}捕获(e:未知){//错误!//属性';to UpperCase';在类型';未知';上不存在。控制台。对数(e.。toUpperCase());if(typeof e=";string";){//works!//我们已将范围缩小到';字符串';类型。控制台。对数(e.。toUpperCase());}}。
虽然catch变量的类型在默认情况下不会更改,但我们可以考虑在将来使用一个新的--Strict模式标志,这样用户就可以选择接受此行为。同时,应该可以编写一条lint规则来强制catch变量有一个显式注释:any或:unked。
在使用JSX时,片段是一种允许我们返回多个子元素的JSX元素类型。当我们第一次在TypeScript中实现片段时,我们对其他库将如何利用它们并没有什么很好的想法。如今,大多数其他鼓励使用JSX并支持片段的库都有类似的API形状。
例如,下面的tsconfig.json文件告诉tyescript以与React兼容的方式转换JSX,但是将每个调用切换为h而不是React.createElement,并使用Fragment而不是React.Fragment。
如果您需要在每个文件的基础上拥有不同的JSX工厂,您可以利用新的/**@jsxFrag*/杂注注释。例如,以下…。
//注意:这些杂注注释需要//使用JSDoc样式的多行语法编写才能生效。/**@JSX h*//**@jsxFrag Frag*/从";preact&34;导入{h,Fragment};let Stuff=<;>;<;div>;Hello<;/div>;<;/>;
//注意:这些杂注注释需要//使用JSDoc样式的多行语法编写才能生效。/**@JSX h*//**@jsxFrag Frag片段*/从";preact";导入{h,Fragment};让Stuff=h(Fragment,NULL,h(";div";,NULL,";Hello";));
我们非常感谢社区成员Noj Vek发出这个拉请求,并耐心地与我们的团队合作。
以前,在使用--noEmitOnError标志时,在上一次编译之后编译程序时,在--Incremental下出错会非常慢。这是因为基于--noEmitOnError标志,上次编译中的任何信息都不会缓存在.tsbuildinfo文件中。
TypeScript4.0改变了这一点,在这些场景中提供了极大的速度提升,并反过来改进了--构建模式场景(这意味着--增量和--noEmitOnError)。
TypeScript4.0允许我们在仍然利用--增量编译的情况下使用--noEmit标志。这在以前是不允许的,因为增量需要发出.tsbuildinfo文件;然而,启用更快的增量构建的用例足够重要,可以为所有用户启用。
Typecript编译器不仅增强了大多数主要编辑器中的TypeScript本身的编辑体验,而且还增强了Visual Studio系列编辑器中的JavaScript体验。出于这个原因,我们的大部分工作都集中在改进编辑器场景上-这是您作为开发人员花费大部分时间的地方。
Visual Studio代码支持选择不同版本的TypeScript。或者,还有JavaScript/TypeScript Nighly扩展可以保持在最前沿(通常非常稳定)。
您可以查看支持TypeScript的编辑器的部分列表,以详细了解您最喜欢的编辑器是否支持使用新版本。
TypeScript的编辑支持现在可以识别声明何时被标记为/**@deposated*JSDoc注释。这些信息出现在完成列表中,并作为编辑可以特殊处理的建议诊断。在VS Code这样的编辑器中,不推荐使用的值通常显示为删除样式,如下所示。
多亏了王文禄,才有了这项新功能。有关更多详细信息,请参阅拉取请求。
我们从用户那里听到的一个具体反馈是启动时间慢,特别是在较大的项目上。具体地说,罪魁祸首通常是一个称为项目加载的过程,该过程与我们编译器的程序构造步骤大致相同。这是从一组初始文件开始、解析它们、解析它们的依赖关系、解析这些依赖关系、解析这些依赖关系的依赖关系等等的过程。这最终会花费相当多的时间。项目越大,在获得基本编辑器操作(如转到定义、代码完成和快速信息)之前,可能会遇到更糟糕的启动延迟。
这就是为什么我们一直致力于为编辑提供部分体验的新模式,直到完整的语言服务体验加载完毕。其核心思想是编辑可以运行一个轻量级的局部服务器,该服务器只有单个文件的世界视图。这一直是编辑器的一个选择,但是TypeScript4.0将功能扩展到语义操作(而不仅仅是语法操作)。虽然这意味着服务器的信息有限(因此并非每个操作都将完全完成)-这通常足以在您第一次打开编辑器时完成一些基本代码、快速信息、签名帮助和转到定义。
虽然很难确定根据硬件、操作系统和项目大小您将看到什么样的改进,但是今天我们已经看到机器在Visual Studio代码库中的文件上花费20秒到1分钟的时间来响应TypeScript。相比之下,这种新模式似乎将打字稿在代码库上交互的时间缩短到了2-5秒之间。
目前唯一支持此模式的编辑器是Visual Studio Code Insiers,您可以按照以下步骤试用。
将Visual Studio代码内部人员配置为使用测试版,或安装面向Visual Studio代码内部人员的JavaScript和TypeScript夜间扩展。
添加以下行://编辑会说动态是一个未知选项,//但现在不用担心。它仍然是动态的experimental.";typescript.tsserver.useSeparateSyntaxServer";:,
用户体验和功能还有改进的空间--无论是在编辑器方面还是在语言支持方面。例如,虽然部分编辑支持已经加载并正在工作,但您仍然会在状态栏中看到正在初始化JS/TS语言功能。您可以忽略这一点,因为操作仍将由该部分模式供电。我们也有一个正在改进的清单,我们正在寻找更多关于您认为可能有用的反馈。
有关更多信息,您可以查看原始提案、实现拉入请求以及后续元问题。
自动导入是一项非常棒的功能,它使编码成为。
..