今天,我们非常高兴地宣布TypeScript4.0已经面市!随着我们更深入地研究可表现性、生产力和可伸缩性,该语言版本代表了我们的下一代Tyescript版本。
如果您不熟悉TypeScript,那么它是一种通过添加静态类型语法而构建在JavaScript之上的语言。其想法是,通过写下值的类型以及它们的使用位置,您可以使用TypeScript来键入-检查代码,并在运行代码之前(甚至在保存文件之前)告诉您有关错误的信息。然后,您可以使用TypeScript编译器从代码中剥离类型,留下干净、可读的JavaScript,可以在任何地方运行。除了检查之外,TypeScript还使用静态类型来支持强大的编辑器工具,如自动完成、代码导航、重构等。事实上,如果您在Visual Studio Code或Visual Studio之类的编辑器中使用过JavaScript,那么您已经在使用由类型和TypeScript提供支持的体验了。你可以在我们的网站上了解更多关于这一切的信息。
在TypeScript4.0中,没有重大的突破性变化。事实上,如果您是这门语言的新手,现在是开始使用它的最佳时机。这个社区已经在这里,而且还在不断增长,有可用的代码和很好的新资源可供学习。还有一件事要记住:尽管我们带来了所有的好东西4.0,你真的只需要知道打字的基础知识就可以提高工作效率!
如果您已经在项目中使用TypeScript,则可以通过NuGet获取它,也可以通过以下命令使用NPM:
TypeScript是当今许多人JavaScript堆栈的核心部分。在NPM上,TypeScript在7月份首次月度下载量超过5000万次!虽然我们认识到总有增长和改进的空间,但很明显,大多数人真的很喜欢用打字稿编写代码。StackOverflow最新的开发者调查显示,打字文字是第二大最受欢迎的语言。在最近的JS状态调查中,大约89%使用TypeScript的开发人员表示他们会再次使用它。
值得深入研究一下我们是如何走到这一步的。在过去的两个主要版本中,我们回顾了这些年来闪耀的一些亮点。对于TypeScript4.0,我们将保持这一传统。
回顾3.0以后的版本,变化之多令人眼花缭乱,但TypeScript3.0本身就是一记重磅炸弹。统一元组类型和参数列表是一大亮点,它在函数上启用了大量现有的JavaScript模式。该版本还提供了项目参考,以帮助扩展、组织和跨代码库共享。一个影响很大的小变化是,3.0引入了一个类型安全的替代方案来替代任何称为未知的东西。
TypeScript 3.1扩展了映射类型的功能以处理元组和数组类型,并使将属性附加到函数变得非常容易,而无需求助于已不再使用的特定于TypeScript的运行时功能。
TypeScript3.2允许对象在泛型类型上传播,并通过严格键入bind、call和Apply来利用3.0的功能更好地对函数进行元编程建模。TypeScript3.3在3.2版之后关注了一些稳定性,但在使用联合类型方法时也带来了生活质量的改进,并在构建模式下添加了文件增量构建。
在3.4版中,我们更倾向于支持函数模式,更好地支持不可变的数据结构,并改进了对高阶泛型函数的推断。最大的优点是,此版本引入了--incremental标志,这是一种通过避免在每次运行类型脚本时完全重新生成而无需项目引用来获得更快编译和类型检查的方法。
在TypeScript3.5和3.6中,我们看到了一些类型系统规则的收紧,以及更智能的兼容性检查规则。
TypeScript3.7是一个非常值得注意的版本,因为它提供了新类型系统特性和ECMAScript特性的丰富组合。从类型系统方面,我们看到了递归类型别名引用和对断言样式函数的支持,这两者都是独特的类型系统特性。在JavaScript方面,该版本带来了可选的链接和合并,这是TypeScript和JavaScript用户都非常需要的两个特性,在TC39中得到了我们团队的部分支持。
最近,3.8和3.9引入了纯类型导入/导出,以及ECMAScript特性,如私有字段、模块中的顶级等待和新的export*语法。这些版本还提供了性能和可伸缩性优化。
我们甚至还没有涉及我们的语言服务、我们的基础设施、我们的网站和其他核心项目中的所有工作,这些对打字体验来说都是令人难以置信的有价值的。除了核心团队的项目,还有生态系统中令人难以置信的贡献者社区,他们推动体验向前发展,并帮助DefinitelyTyped,甚至Tyescript本身。DefinitelyTyped在2012年首次启动时只有80个拉取请求,在年底时有所回升。2019年,它的拉取请求超过了8300次,这仍然让我们感到震惊。这些贡献是打字体验的基础,所以我们感谢这样一个忙碌而热切的社区,他们一直在改善生态系统,并推动我们不断改进。
所有这些都将我们带到了4.0版本!所以,不要再费劲了,让我们深入了解一下最新的内容吧!
考虑JavaScript中一个名为CONCAT的函数,该函数接受两个数组或元组类型,并将它们连接在一起形成一个新的数组。
还要考虑Tail,它接受一个数组或元组,并返回除第一个元素之外的所有元素。
对于Concat,我们在该语言的旧版本中所能做的唯一有效的事情就是尝试并编写一些重载。
函数拼接(arr1:[],arr2:[]):[];函数拼接<;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,4,...String[]]const R2=Tail([...。我的元组,..。MyArray]as const);
第二个变化是REST元素可以出现在元组中的任何位置-而不仅仅是末尾!
Type Strings=[String,String];type Numbers=[Number,Number];//[String,String,Number,Number,Boolean]type StrNumNumBool=[...。弦,..。数字,布尔值];
请注意,当我们在没有已知长度的类型中展开时,得到的类型也变得无界,并且以下所有元素都包含在得到的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接受一个函数-让我们称它为f-以及f所需的一些初始参数。然后,它返回一个新函数,该函数接受f仍然需要的任何其他参数,并在接收到这些参数时调用f。
TypeScript4.0改进了REST参数和REST元组元素的推理过程,这样我们就可以键入它并使其“正常工作”。
类型arr=只读未知[];函数partalCall<;T扩展arr,U扩展arr,R>;(f:(...。参数:[...。T,..。U])=>;R,...。HeadArgs:t){return(...。尾参数:u)=>;f(...。HeadArgs,..。尾部参数)}。
在本例中,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";);//~//错误!';字符串';类型的参数不能赋给';布尔';类型的参数。
可变元组类型支持许多新的令人兴奋的模式,特别是在函数组合方面。我们希望能够利用它来更好地检查JavaScript的内置绑定方法的类型。一些其他推理改进和模式也涉及到这一点,如果您有兴趣了解更多信息,可以查看各种元组的Pull请求。
改善有关元组类型和参数列表的体验非常重要,因为它允许我们围绕常见的JavaScript习惯用法进行强类型验证-实际上只是对参数列表进行切片和切分,然后将它们传递给其他函数。我们可以将元组类型用于REST参数的想法是至关重要的。
例如,以下函数使用元组类型作为REST参数…。
不过,有一个地方的差异开始变得明显起来:可读性。在第一个示例中,我们没有第一个和第二个元素的参数名称。虽然这些对类型检查没有影响,但是元组位置上缺少标签会使它们更难使用-更难传达我们的意图。
为了加深参数列表和元组类型之间的联系,REST元素和可选元素的语法反映了参数列表的语法。
使用带标签的元组时有几条规则。例如,在标记元组元素时,元组中的所有其他元素也必须被标记。
Type Bar=[first:string,number];//~//错误!元组成员必须都有名称,或者都没有名称。
值得注意的是,标签在解构时不要求我们以不同的方式命名变量。它们在那里纯粹是为了文档和工具。
函数foo(x:[First:String,Second:Number]){//...//注意:我们不需要将它们命名为';First';和';Second';let[a,b]=x;//...}。
总体而言,当利用元组和参数列表周围的模式以及以类型安全的方式实现重载时,带标签的元组是很方便的。事实上,TypeScript的编辑器支持在可能的情况下会尝试将它们显示为重载。
当启用了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支持一个新的ECMAScript特性来添加三个新的赋值操作符:&;&;=、||=和??=。
这些运算符非常适合替换用户可能编写如下代码的任何示例:
甚至还有一些我们看到的(或者,呃,我们自己编写的)懒惰地初始化值的模式,只有在需要时才会这样做。
在极少数情况下,您使用有副作用的getter或setter,值得注意的是,这些操作符仅在必要时执行赋值。从这个意义上说,不仅运算符的右侧“短路”,赋值本身也是“短路”的。
OBJ。Prop||=foo();//大致相当于以下任一obj。道具||(对象。Prop=foo());如果(!OBJ。道具){Obj.。Prop=foo();}
尝试运行以下示例,以了解这与始终执行分配有何不同。
Const obj={get prop(){console.。Log(";getter已运行";);//替换我!返回数学。Random()<;0.5;},设置属性(_val:布尔值){控制台。Log(";setter已运行";);}};函数foo(){console。Log(";右侧求值";);返回true;}控制台。Log(";这一个总是运行setter";);obj。属性=对象。Prop||foo();控制台。Log(";这个*有时*运行setter";);obj。Prop||=foo();
我们非常感谢社区成员王文禄的贡献!
自从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*/杂注注释。例如。
.