TypeScript 4.1

2020-11-21 18:11:51

如果您不熟悉TypeScript,那么它是一种通过在JavaScript上添加类型声明和注释的语法而构建的语言。 TypeScript编译器可以使用此语法对我们的代码进行类型检查,然后输出可在许多不同的运行时上运行的清晰可读的JavaScript。由于TypeScript具有跨编辑器的丰富编辑功能,因此静态类型检查可以在代码运行之前甚至保存文件之前告诉我们代码中的错误。但是,除了进行错误检查外,TypeScript还可以在某些您喜欢的编辑器中为TypeScript和JavaScript提供诸如完成,快速修复和重构之类的功能。实际上,如果您已经在使用Visual Studio或Visual Studio Code,则在编写JavaScript代码时可能已经在使用TypeScript!因此,如果您有兴趣了解更多信息,请访问我们的网站!

但是,如果您已经在项目中使用TypeScript,则可以通过NuGet获取它,也可以通过以下命令使用npm:

对于此版本,我们提供了一些令人兴奋的新功能,新的检查标志,编辑器效率更新和速度改进。让我们来看看4.1带来了什么!

TypeScript中的字符串文字类型使我们可以对需要一组特定字符串的函数和API进行建模。

function setVerticalAlignment ( color: "top" | "middle" | "bottom" ) { // ... } setVerticalAlignment ( "middel" ) ; // ~~~~~~~~ // error: Argument of type '"middel"' is not assignable to // parameter of type '"top" | "middle" | "bottom"'.

我们还喜欢将字符串文字用作映射类型中的属性名称。从这个意义上讲,它们也可用作构建基块。

type Options = { [ K in "noImplicitAny" | "strictNullChecks" | "strictFunctionTypes" ]?: boolean } ; // same as // type Options = { // noImplicitAny?: boolean, // strictNullChecks?: boolean, // strictFunctionTypes?: boolean // };

但是在另一个地方,字符串文字类型可以用作构建块:构建其他字符串文字类型。

这就是TypeScript 4.1带来模板文字字符串类型的原因。它具有与JavaScript中的模板文字字符串相同的语法,但用于类型位置。当将它与具体文字类型一起使用时,它将通过串联内容来产生新的字符串文字类型。

type World = "world" ; type Greeting = `hello ${ World }` ; // same as // type Greeting = "hello world";

当工会担任替代职位时会发生什么?它产生可以由每个联合成员表示的每个可能的字符串文字的集合。

type Color = "red" | "blue" ; type Quantity = "one" | "two" ; type SeussFish = ` ${ Quantity | Color } fish` ; // same as // type SeussFish = "one fish" | "two fish" // | "red fish" | "blue fish";

This can be used beyond cute examples in release notes. For example, several libraries for UI components have a way to specify both vertical and horizontal alignment in their APIs, often with both at once using a single string like "bottom-right". Between vertically aligning with "top", "middle", and "bottom", and horizontally aligning with "left", "center", and "right", there are 9 possible strings where each of the former strings is connected with each of the latter strings using a dash.

type VerticalAlignment = "top" | "middle" | "bottom" ; type HorizontalAlignment = "left" | "center" | "right" ; // Takes // | "top-left" | "top-center" | "top-right" // | "middle-left" | "middle-center" | "middle-right" // | "bottom-left" | "bottom-center" | "bottom-right" declare function setAlignment ( value: `${ VerticalAlignment } -${ HorizontalAlignment }` ): void ; setAlignment ( "top-left" ) ; // works! setAlignment ( "top-middel" ) ; // error! setAlignment ( "top-pot" ) ; // error! but good doughnuts if you're ever in Seattle

尽管有很多此类API的示例,但由于我们可以手动将其编写出来,因此这仍然只是一个玩具示例。实际上,对于9个字符串,这可能很好。但是当您需要大量字符串时,应考虑提前自动生成它们,以节省每次类型检查的工作(或仅使用字符串,这将更容易理解)。

一些实际价值来自动态创建新的字符串文字。例如,想象一个makeWatchedObject API,它接受一个对象并产生一个几乎相同的对象,但是具有一个新的on方法来检测属性的变化。

let person = makeWatchedObject ( { firstName: "Homer" , age: 42 , // give-or-take location: "Springfield" , } ) ; person . on ( "firstNameChanged" , ( ) => { console . log ( `firstName was changed!` ) ; } ) ;

Notice that on listens on the event "firstNameChanged", not just "firstName". How would we type this?

type PropEventSource < T > = { on ( eventName: `${ string & keyof T } Changed` , callback: ( ) => void ): void ; } ; /// Create a "watched object" with an 'on' method /// so that you can watch for changes to properties. declare function makeWatchedObject < T > ( obj: T ): T & PropEventSource < T > ;

我们还可以对模板文字类型做一些特殊的事情:我们可以从替换位置推断出来。我们可以使最后一个示例通用,以便从eventName字符串的各个部分进行推断,以找出关联的属性。

type PropEventSource < T > = { on < K extends string & keyof T > ( eventName: `${ K } Changed` , callback: ( newValue: T [ K ] ) => void ): void ; } ; declare function makeWatchedObject < T > ( obj: T ): T & PropEventSource < T > ; let person = makeWatchedObject ( { firstName: "Homer" , age: 42 , location: "Springfield" , } ) ; // works! 'newName' is typed as 'string' person . on ( "firstNameChanged" , newName => { // 'newName' has the type of 'firstName' console . log ( `new name is ${newName . toUpperCase ( ) }` ) ; } ) ; // works! 'newAge' is typed as 'number' person . on ( "ageChanged" , newAge => { if ( newAge < 0 ) { console . log ( "warning! negative age" ) ; } } )

Here we made on into a generic method. When a user calls with the string "firstNameChanged', TypeScript will try to infer the right type for K. To do that, it will match K against the content prior to "Changed" and infer the string "firstName". Once TypeScript figures that out, the on method can fetch the type of firstName on the original object, which is string in this case. Similarly, when we call with "ageChanged", it finds the type for the property age which is number).

推理可以以不同的方式组合,通常是对字符串进行解构,并以不同的方式对其进行重构。实际上,为了帮助修改这些字符串文字类型,我们添加了一些新的实用程序类型别名,用于修改字母中的大小写(即转换为小写和大写字符)。

type EnthusiasticGreeting < T extends string > = `${ Uppercase < T > }` type HELLO = EnthusiasticGreeting < "hello" > ; // same as // type HELLO = "HELLO";

新的类型别名为大写,小写,大写和大写。前两个转换字符串中的每个字符,后两个仅转换字符串中的第一个字符。

就像刷新一样,映射类型可以基于任意键创建新的对象类型

type Options = { [ K in "noImplicitAny" | "strictNullChecks" | "strictFunctionTypes" ]?: boolean } ; // same as // type Options = { // noImplicitAny?: boolean, // strictNullChecks?: boolean, // strictFunctionTypes?: boolean // };

/// 'Partial<T>' is the same as 'T', but with each property marked optional. type Partial < T > = { [ K in keyof T ]?: T [ K ] } ;

到目前为止,映射类型只能使用您提供的键来产生新的对象类型。但是,很多时候您希望能够根据输入来创建新键或过滤掉键。

因此,TypeScript 4.1允许您使用新的as子句重新映射映射类型中的键。

type MappedTypeWithNewKeys < T > = { [ K in keyof T as NewKeyType ]: T [ K ] // ^^^^^^^^^^^^^ // This is the new syntax! }

使用这个新的as子句,您可以利用模板文字类型之类的功能轻松地基于旧名称创建属性名称。

type Getters < T > = { [ K in keyof T as `get ${ Capitalize < string & K > }` ]: ( ) => T [ K ] } ; interface Person { name: string ; age: number ; location: string ; } type LazyPerson = Getters < Person > ;

您甚至可以通过从不产生密钥来过滤掉密钥。这意味着在某些情况下,您不必使用额外的Omit帮助程序类型。

// Remove the 'kind' property type RemoveKindField < T > = { [ K in keyof T as Exclude < K , "kind" > ]: T [ K ] } ; interface Circle { kind: "circle" ; radius: number ; } type KindlessCircle = RemoveKindField < Circle > ; // same as // type KindlessCircle = { // radius: number; // };

在JavaScript中,常见的功能是可以扁平化并建立任意级别的容器类型。例如,考虑Promise实例上的.then()方法。 .then(...)解开每个诺言,直到找到一个不像“承诺”那样的值,然后将该值传递给回调。数组上还有一个相对较新的flat方法,可以深入了解要展平的深度。

出于所有实际意图和目的,在TypeScript的类型系统中无法表达这一点。尽管有一些破解方法可以实现,但最终这些类型看起来非常不合理。

这就是为什么TypeScript 4.1减轻了对条件类型的一些限制-以便他们可以对这些模式进行建模。在TypeScript 4.1中,条件类型现在可以立即在其分支中引用自身,从而更容易编写递归类型别名。

例如,如果我们想编写一个类型来获取嵌套数组的元素类型,则可以编写以下deepFlatten类型。

type ElementType < T > = T extends ReadonlyArray < infer U > ? ElementType < U > : T ; function deepFlatten < T extends readonly unknown [ ] > ( x: T ): ElementType < T > [ ] { throw "not implemented" ; } // All of these return the type 'number[]': deepFlatten ( [ 1 , 2 , 3 ] ) ; deepFlatten ( [ [ 1 ] , [ 2 , 3 ] ] ) ; deepFlatten ( [ [ 1 ] , [ [ 2 ] ] , [ [ [ 3 ] ] ] ] ) ;

type Awaited < T > = T extends PromiseLike < infer U > ? Awaited < U > : T ; /// Like `promise.then(...)`, but more accurate in types. declare function customThen < T , U > ( p: Promise < T > , onFulfilled: ( value: Awaited < T > ) => U ): Promise < Awaited < U > > ;

请记住,尽管这些递归类型功能强大,但应负责任地谨慎使用它们。

首先,这些类型可以完成很多工作,这意味着它们可以增加类型检查时间。尝试以Collat​​z猜想或斐波那契数列为模型建模可能很有趣,但不要将其放在npm的.d.ts文件中。

但是,除了计算量大之外,这些类型还可能在足够复杂的输入上达到内部递归深度限制。当达到该递归限制时,将导致编译时错误。通常,最好不要使用这些类型,而要写一些在更实际的示例中失败的东西。

TypeScript具有称为索引签名的功能。这些签名是一种向类型系统发出信号的信号,用户可以访问任意命名的属性。

interface Options { path: string ; permissions: number ; // Extra properties are caught by this index signature. [ propName: string ]: string | number ; } function checkOptions ( opts: Options ) { opts . path // string opts . permissions // number // These are all allowed too! // They have the type 'string | number'. opts . yadda . toString ( ) ; opts [ "foo bar baz" ] . toString ( ) ; opts [ Math . random ( ) ] . toString ( ) ; }

在上面的示例中,“选项”具有索引签名,该索引签名表示未列出的所有已访问属性都应具有字符串|数。这对于假定您知道自己在做什么的乐观代码来说通常很方便,但事实是JavaScript中的大多数值并不支持所有可能的属性名称。例如,大多数类型都不会像前面的示例那样具有Math.random()创建的属性键的值。对于许多用户而言,这种行为是不希望的,并且感觉到它没有利用--strictNullChecks的完全严格检查。

That’s why TypeScript 4.1 ships with a new flag called --noUncheckedIndexedAccess. Under this new mode, every property access (like foo.bar) or indexed access (like foo["bar"]) is considered potentially undefined. That means that in our last example, opts.yadda will have the type string | number | undefined as opposed to just string | number. If you need to access that property, you’ll either have to check for its existence first or use a non-null assertion operator (the postfix ! character).

// Checking if it's really there first. if ( opts . yadda ) { console . log ( opts . yadda . toString ( ) ) ; } // Basically saying "trust me I know what I'm doing" // with the '!' non-null assertion operator. opts . yadda! . toString ( ) ;

使用--noUncheckedIndexedAccess的一个结果是,即使在边界检查循环中,也更严格地检查对数组的索引。

function screamLines ( strs: string [ ] ) { // this will have issues for ( let i = 0 ; i < strs . length ; i ++ ) { console . log ( strs [ i ] . toUpperCase ( ) ) ; // ~~~~~~~ // error! Object is possibly 'undefined'. } }

如果不需要索引,则可以使用for–循环或forEach调用来遍历各个元素。

function screamLines ( strs: string [ ] ) { // this works fine for ( const str of strs ) { console . log ( str . toUpperCase ( ) ) ; } // this works fine strs . forEach ( str => { console . log ( str . toUpperCase ( ) ) ; } ) ; }

这个标志对于捕获越界错误可能很方便,但是对于很多代码来说可能很嘈杂,因此--strict标志不会自动启用它。但是,如果您对这个功能很感兴趣,则可以随意尝试并确定它是否适合您团队的代码库!

使用路径映射是相当普遍的-通常是为了更好地导入,通常是为了模拟Monorepo链接行为。

不幸的是,指定启用路径映射的路径还需要指定一个称为baseUrl的选项,该选项也允许相对于baseUrl到达裸指定符路径。这通常还导致自动导入使用较差的路径。

在TypeScript 4.1中,可以在没有baseUrl的情况下使用path选项。这有助于避免其中一些问题。

以前,如果您要启动一个选中的JavaScript项目,则必须同时设置allowJs和checkJs。这在体验中有点烦人,因此默认情况下,checkJs隐含了allowJs。

TypeScript 4.1通过jsx编译器选项的两个新选项支持React 17即将推出的jsx和jsxs工厂功能:

这些选项分别用于生产和开发编译。通常,一个选项可以从另一个扩展。例如,用于生产构建的tsconfig.json可能如下所示:

// ./src/tsconfig.json { "compilerOptions": { "module": "esnext" , "target": "es2015" , "jsx": "react-jsx" , "strict": true } , "include": [ "./**/*" ] }

JSDoc标记@see标记现在在TypeScript和JavaScript的编辑器中有更好的支持。这使您可以在标记后的虚线名称中使用定位定义之类的功能。例如,在下面的示例中,仅对JSDoc注释中的first或C进行定义即可:

// @filename: first.ts export class C { } // @filename: main.ts import * as first from './first' ; /** * @see first.C */ function related ( ) { }

lib.d.ts可能具有一组更改的API,这可能部分是由于DOM类型是如何自动生成的。一项特定更改是,从ES2016中删除了Reflect.enumerate。

标记为抽象的成员不能再标记为异步。此处的解决方法是删除async关键字,因为调用方只关心返回类型。

Previously, for an expression like foo && somethingElse, the type of foo was any or unknown, the type of the whole that expression would be the type of somethingElse.

declare let foo: unknown ; declare let somethingElse: { someProp: string } ; let x = foo && somethingElse ;

However, in TypeScript 4.1, we are more careful about how we determine this type. Since nothing is known about the type on the left side of the &&, we propagate any and unknown outward instead of the type on the right side.

我们看到的最常见的模式通常是在检查与布尔值的兼容性时,尤其是在谓词函数中。

function isThing ( x: any ): boolean { return x && typeof x === 'object' && x . blah === 'foo' ; }

Often the appropriate fix is to switch from foo && someExpression to !!foo && someExpression.

resolve() ~~~~~~~~~error TS2554: Expected 1 arguments, but got 0. An argument for 'value' was not provided.

这是因为resolve不再具有可选参数,因此默认情况下现在必须为它传递一个值。通常,使用Promises会捕获合法错误。典型的解决方法是为其传递正确的参数,有时还要添加一个显式的类型参数。

However, sometimes resolve() really does need to be called without an argument. In these cases, we can give Promise an explicit void generic type argument (i.e. write it out as Promise<void>). This leverages new functionality in TypeScript 4.1 where a potentially- void trailing parameter can become optional.

在JavaScript中,对象传播(如{... foo})不会对虚假值起作用。因此,在类似{... foo}的代码中,如果foo为null或未定义,则会跳过foo。

interface Person { name: string ; age: number ; location: string ; } interface Animal { name: string ; owner: Person ; } function copyOwner ( pet?: Animal ) { return { ... ( pet && pet . owner ) , otherStuff: 123 } } // We could also use optional chaining here: function copyOwner ( pet?: Animal ) { return { ... ( pet? . owner ) , otherStuff: 123 } }

在这里,如果定义了pet,则pet.owner的属性将散布在其中;否则,不会将任何属性散布到返回的对象中。

{x:数字} | {x:数字,名称:字符串,年龄:数字,位置:字符串}

这准确地模拟了操作的发生方式:如果定义了pet,Person的所有属性都将存在;其他

......