可选链式调用
¥Optional Chaining
在我们的问题跟踪器中,可选链式调用是 问题 #16。自那时起,TypeScript 问题跟踪器上已收到超过 23,000 个问题。
¥Optional chaining is issue #16 on our issue tracker. For context, there have been over 23,000 issues on the TypeScript issue tracker since then.
从本质上讲,可选链式调用让我们能够编写这样的代码:如果遇到 null
或 undefined
,TypeScript 可以立即停止运行某些表达式。可选链式调用中的亮点是用于访问可选属性的新 ?.
运算符。当我们编写如下代码时
¥At its core, optional chaining lets us write code where TypeScript can immediately stop running some expressions if we run into a null
or undefined
.
The star of the show in optional chaining is the new ?.
operator for optional property accesses.
When we write code like
ts
let x = foo?.bar.baz();
这是一种说法,表示当 foo
被定义时,将计算 foo.bar.baz()
;但是当 foo
是 null
或 undefined
时,停止我们正在做的事情,直接返回 undefined
。
¥this is a way of saying that when foo
is defined, foo.bar.baz()
will be computed; but when foo
is null
or undefined
, stop what we’re doing and just return undefined
.”
更直白地说,该代码片段与编写以下内容相同。
¥More plainly, that code snippet is the same as writing the following.
ts
let x = foo === null || foo === undefined ? undefined : foo.bar.baz();
请注意,如果 bar
是 null
或 undefined
,我们的代码在访问 baz
时仍然会出错。同样,如果 baz
是 null
或 undefined
,我们会在调用处遇到错误。?.
仅检查其左侧的值是 null
还是 undefined
。 - 任何后续属性也不应该被扩展。
¥Note that if bar
is null
or undefined
, our code will still hit an error accessing baz
.
Likewise, if baz
is null
or undefined
, we’ll hit an error at the call site.
?.
only checks for whether the value on the left of it is null
or undefined
- not any of the subsequent properties.
你可能会发现自己正在使用 ?.
替换大量使用 &&
运算符执行重复空值检查的代码。
¥You might find yourself using ?.
to replace a lot of code that performs repetitive nullish checks using the &&
operator.
ts
// Beforeif (foo && foo.bar && foo.bar.baz) {// ...}// After-ishif (foo?.bar?.baz) {// ...}
请记住,?.
的操作与 &&
的操作不同,因为 &&
会专门针对 “falsy” 值(例如空字符串、0
、NaN
以及 false
)进行操作,但这是该构造的一个有意为之的特性。它不会对 0
或空字符串等有效数据进行短路。
¥Keep in mind that ?.
acts differently than those &&
operations since &&
will act specially on “falsy” values (e.g. the empty string, 0
, NaN
, and, well, false
), but this is an intentional feature of the construct.
It doesn’t short-circuit on valid data like 0
or empty strings.
可选链式调用还包含另外两个操作。首先,可选元素访问的作用类似于可选属性访问,但允许我们访问非标识符属性(例如任意字符串、数字和符号):
¥Optional chaining also includes two other operations. First there’s the optional element access which acts similarly to optional property accesses, but allows us to access non-identifier properties (e.g. arbitrary strings, numbers, and symbols):
ts
/*** Get the first element of the array if we have an array.* Otherwise return undefined.*/function tryGetFirstElement<T>(arr?: T[]) {return arr?.[0];// equivalent to// return (arr === null || arr === undefined) ?// undefined :// arr[0];}
还有一个可选的调用,它允许我们在表达式不是 null
或 undefined
时有条件地调用它们。
¥There’s also optional call, which allows us to conditionally call expressions if they’re not null
or undefined
.
ts
async function makeRequest(url: string, log?: (msg: string) => void) {log?.(`Request started at ${new Date().toISOString()}`);// roughly equivalent to// if (log != null) {// log(`Request started at ${new Date().toISOString()}`);// }const result = (await fetch(url)).json();log?.(`Request finished at ${new Date().toISOString()}`);return result;}
可选链的 “short-circuiting” 行为限制了属性访问、调用和元素访问。 - 它不会进一步扩展这些表达式。换句话说,
¥The “short-circuiting” behavior that optional chains have is limited property accesses, calls, element accesses - it doesn’t expand any further out from these expressions. In other words,
ts
let result = foo?.bar / someComputation();
不会阻止除法或 someComputation()
调用的发生。这相当于
¥doesn’t stop the division or someComputation()
call from occurring.
It’s equivalent to
ts
let temp = foo === null || foo === undefined ? undefined : foo.bar;let result = temp / someComputation();
这可能会导致 undefined
被除数,这就是为什么在 strictNullChecks
中会出现以下错误。
¥That might result in dividing undefined
, which is why in strictNullChecks
, the following is an error.
ts
function barPercentage(foo?: { bar: number }) {return foo?.bar / 100;// ~~~~~~~~// Error: Object is possibly undefined.}
更多详细信息,你可以使用 阅读提案信息 和 查看原始拉取请求。
¥More more details, you can read up on the proposal and view the original pull request.
空值合并
¥Nullish Coalescing
空值合并运算符是另一个即将推出的 ECMAScript 功能,它与可选链密切相关,我们的团队一直致力于在 TC39 中推广该功能。
¥The nullish coalescing operator is another upcoming ECMAScript feature that goes hand-in-hand with optional chaining, and which our team has been involved with championing in TC39.
你可以将其视为此功能 - ??
运算符 - 在处理 null
或 undefined
时,将 “回退” 转换为默认值。当我们编写如下代码时
¥You can think of this feature - the ??
operator - as a way to “fall back” to a default value when dealing with null
or undefined
.
When we write code like
ts
let x = foo ?? bar();
这是一种新的说法,表示当 “present” 为 foo
时,将使用 foo
的值;但是当它是 null
或 undefined
时,就在其位置计算 bar()
。
¥this is a new way to say that the value foo
will be used when it’s “present”;
but when it’s null
or undefined
, calculate bar()
in its place.
同样,上述代码等同于以下代码。
¥Again, the above code is equivalent to the following.
ts
let x = foo !== null && foo !== undefined ? foo : bar();
当尝试使用默认值时,??
运算符可以替代 ||
的使用。例如,以下代码片段尝试获取上次保存在 localStorage
中的卷(如果曾经保存过);但是,由于使用了 ||
,它存在一个 bug。
¥The ??
operator can replace uses of ||
when trying to use a default value.
For example, the following code snippet tries to fetch the volume that was last saved in localStorage
(if it ever was);
however, it has a bug because it uses ||
.
ts
function initializeAudio() {let volume = localStorage.volume || 0.5;// ...}
当 localStorage.volume
设置为 0
时,页面会将卷设置为 0.5
,这是非预期的。??
避免了 0
、NaN
和 ""
被视为假值时的一些意外行为。
¥When localStorage.volume
is set to 0
, the page will set the volume to 0.5
which is unintended.
??
avoids some unintended behavior from 0
, NaN
and ""
being treated as falsy values.
非常感谢社区成员 Wenlu Wang 和 Titian Cernicova Dragomir 实现了此功能!更多详情,请访问 查看他们的拉取请求 和 空值合并提案仓库。
¥We owe a large thanks to community members Wenlu Wang and Titian Cernicova Dragomir for implementing this feature! For more details, check out their pull request and the nullish coalescing proposal repository.
断言函数
¥Assertion Functions
如果发生意外情况,有一组特定的函数会 throw
错误。它们被称为 “assertion” 函数。例如,Node.js 有一个专门用于此的函数,名为 assert
。
¥There’s a specific set of functions that throw
an error if something unexpected happened.
They’re called “assertion” functions.
As an example, Node.js has a dedicated function for this called assert
.
js
assert(someValue === 42);
在此示例中,如果 someValue
不等于 42
,则 assert
将抛出 AssertionError
异常。
¥In this example if someValue
isn’t equal to 42
, then assert
will throw an AssertionError
.
JavaScript 中的断言通常用于防止传入不正确的类型。例如,
¥Assertions in JavaScript are often used to guard against improper types being passed in. For example,
js
function multiply(x, y) {assert(typeof x === "number");assert(typeof y === "number");return x * y;}
不幸的是,在 TypeScript 中,这些检查永远无法正确编码。对于弱类型代码,这意味着 TypeScript 的检查较少,而对于稍微保守的代码,它通常会迫使用户使用类型断言。
¥Unfortunately in TypeScript these checks could never be properly encoded. For loosely-typed code this meant TypeScript was checking less, and for slightly conservative code it often forced users to use type assertions.
ts
function yell(str) {assert(typeof str === "string");return str.toUppercase();// Oops! We misspelled 'toUpperCase'.// Would be great if TypeScript still caught this!}
另一种方法是重写代码,以便语言可以分析它,但这并不方便。
¥The alternative was to instead rewrite the code so that the language could analyze it, but this isn’t convenient.
ts
function yell(str) {if (typeof str !== "string") {throw new TypeError("str should have been a string.");}// Error caught!return str.toUppercase();}
TypeScript 的最终目标是以最少的破坏性方式对现有的 JavaScript 构造函数进行类型化。因此,TypeScript 3.7 引入了一个名为 “断言签名” 的新概念,它对这些断言函数进行了建模。
¥Ultimately the goal of TypeScript is to type existing JavaScript constructs in the least disruptive way. For that reason, TypeScript 3.7 introduces a new concept called “assertion signatures” which model these assertion functions.
第一种断言签名模仿了 Node 的 assert
函数的工作方式。它确保正在检查的任何条件在包含作用域的剩余部分都必须为真。
¥The first type of assertion signature models the way that Node’s assert
function works.
It ensures that whatever condition is being checked must be true for the remainder of the containing scope.
ts
function assert(condition: any, msg?: string): asserts condition {if (!condition) {throw new AssertionError(msg);}}
asserts condition
规定,如果 assert
返回,则传递给 condition
参数的任何值都必须为 true(否则会抛出错误)。这意味着对于其余的作用域,该条件必须为真。例如,使用此断言函数意味着我们确实捕获了原始 yell
示例。
¥asserts condition
says that whatever gets passed into the condition
parameter must be true if the assert
returns (because otherwise it would throw an error).
That means that for the rest of the scope, that condition must be truthy.
As an example, using this assertion function means we do catch our original yell
example.
ts
function yell(str) {assert(typeof str === "string");return str.toUppercase();// ~~~~~~~~~~~// error: Property 'toUppercase' does not exist on type 'string'.// Did you mean 'toUpperCase'?}function assert(condition: any, msg?: string): asserts condition {if (!condition) {throw new AssertionError(msg);}}
另一种断言签名不会检查条件,而是告诉 TypeScript 特定变量或属性具有不同的类型。
¥The other type of assertion signature doesn’t check for a condition, but instead tells TypeScript that a specific variable or property has a different type.
ts
function assertIsString(val: any): asserts val is string {if (typeof val !== "string") {throw new AssertionError("Not a string!");}}
此处,asserts val is string
确保在调用 assertIsString
之后,传入的任何变量都将被识别为 string
。
¥Here asserts val is string
ensures that after any call to assertIsString
, any variable passed in will be known to be a string
.
ts
function yell(str: any) {assertIsString(str);// Now TypeScript knows that 'str' is a 'string'.return str.toUppercase();// ~~~~~~~~~~~// error: Property 'toUppercase' does not exist on type 'string'.// Did you mean 'toUpperCase'?}
这些断言签名与编写类型谓词签名非常相似:
¥These assertion signatures are very similar to writing type predicate signatures:
ts
function isString(val: any): val is string {return typeof val === "string";}function yell(str: any) {if (isString(str)) {return str.toUppercase();}throw "Oops!";}
就像类型谓词签名一样,这些断言签名表达能力极强。我们可以用这些来表达一些相当复杂的想法。
¥And just like type predicate signatures, these assertion signatures are incredibly expressive. We can express some fairly sophisticated ideas with these.
ts
function assertIsDefined<T>(val: T): asserts val is NonNullable<T> {if (val === undefined || val === null) {throw new AssertionError(`Expected 'val' to be defined, but received ${val}`);}}
了解更多关于断言签名的信息,请参阅 查看原始拉取请求。
¥To read up more about assertion signatures, check out the original pull request.
更好地支持返回 never
的函数
¥Better Support for never
-Returning Functions
作为断言签名工作的一部分,TypeScript 需要对调用位置和调用哪些函数进行更多编码。这让我们有机会扩展对另一类函数的支持:返回 never
的函数。
¥As part of the work for assertion signatures, TypeScript needed to encode more about where and which functions were being called.
This gave us the opportunity to expand support for another class of functions: functions that return never
.
任何返回 never
的函数的目的都是永不返回。它指示抛出了异常、发生了暂停错误情况或程序已退出。例如,process.exit(...)
in @types/node
指定返回 never
。
¥The intent of any function that returns never
is that it never returns.
It indicates that an exception was thrown, a halting error condition occurred, or that the program exited.
For example, process.exit(...)
in @types/node
is specified to return never
.
为了确保函数永远不会返回 undefined
或从所有代码路径有效返回,TypeScript 需要一些语法信号。 - 函数末尾的 return
或 throw
。所以用户发现自己在失败函数中 return
化了。
¥In order to ensure that a function never potentially returned undefined
or effectively returned from all code paths, TypeScript needed some syntactic signal - either a return
or throw
at the end of a function.
So users found themselves return
-ing their failure functions.
ts
function dispatch(x: string | number): SomeType {if (typeof x === "string") {return doThingWithString(x);} else if (typeof x === "number") {return doThingWithNumber(x);}return process.exit(1);}
现在,当调用这些返回 never
的函数时,TypeScript 会识别出它们会影响控制流图并进行处理。
¥Now when these never
-returning functions are called, TypeScript recognizes that they affect the control flow graph and accounts for them.
ts
function dispatch(x: string | number): SomeType {if (typeof x === "string") {return doThingWithString(x);} else if (typeof x === "number") {return doThingWithNumber(x);}process.exit(1);}
与断言函数一样,你可以使用 在同一个拉取请求中阅读更多信息。
¥As with assertion functions, you can read up more at the same pull request.
(更多)递归类型别名
¥(More) Recursive Type Aliases
类型别名在 “recursively” 引用方式上一直存在限制。原因是,任何类型别名的使用都需要能够用其别名替换自身。在某些情况下,这是不可能的,因此编译器会拒绝某些递归别名,例如:
¥Type aliases have always had a limitation in how they could be “recursively” referenced. The reason is that any use of a type alias needs to be able to substitute itself with whatever it aliases. In some cases, that’s not possible, so the compiler rejects certain recursive aliases like the following:
ts
type Foo = Foo;
这是一个合理的限制,因为任何使用 Foo
的文件都需要替换为 Foo
,而 Foo
又需要替换为 Foo
,而 Foo
又需要替换为 Foo
……好了,希望你明白了!最终,没有一个类型可以替代 Foo
。
¥This is a reasonable restriction because any use of Foo
would need to be replaced with Foo
which would need to be replaced with Foo
which would need to be replaced with Foo
which… well, hopefully you get the idea!
In the end, there isn’t a type that makes sense in place of Foo
.
这相当符合 与其他语言处理类型别名的方式一致 规范,但它确实引发了一些关于用户如何利用该功能的略微令人惊讶的场景。例如,在 TypeScript 3.6 及更早版本中,以下操作会导致错误。
¥This is fairly consistent with how other languages treat type aliases, but it does give rise to some slightly surprising scenarios for how users leverage the feature. For example, in TypeScript 3.6 and prior, the following causes an error.
ts
type ValueOrArray<T> = T | Array<ValueOrArray<T>>;// ~~~~~~~~~~~~// error: Type alias 'ValueOrArray' circularly references itself.
这很奇怪,因为从技术上讲,用户可以通过引入接口来编写实际上相同的代码,这本身并没有错。
¥This is strange because there is technically nothing wrong with any use users could always write what was effectively the same code by introducing an interface.
ts
type ValueOrArray<T> = T | ArrayOfValueOrArray<T>;interface ArrayOfValueOrArray<T> extends Array<ValueOrArray<T>> {}
由于接口(以及其他对象类型)引入了一层间接层,并且其完整结构无需急切构建,因此 TypeScript 可以轻松处理这种结构。
¥Because interfaces (and other object types) introduce a level of indirection and their full structure doesn’t need to be eagerly built out, TypeScript has no problem working with this structure.
但是,引入接口的解决方法对用户来说并不直观。原则上,直接使用 Array
的原始 ValueOrArray
版本确实没有任何问题。如果编译器稍微偏向 “lazier”,并且仅在必要时计算 Array
的类型参数,那么 TypeScript 就可以正确表达这些参数。
¥But workaround of introducing the interface wasn’t intuitive for users.
And in principle there really wasn’t anything wrong with the original version of ValueOrArray
that used Array
directly.
If the compiler was a little bit “lazier” and only calculated the type arguments to Array
when necessary, then TypeScript could express these correctly.
这正是 TypeScript 3.7 引入的功能。在类型别名的 “顶层” 处,TypeScript 将推迟解析类型参数以允许这些模式。
¥That’s exactly what TypeScript 3.7 introduces. At the “top level” of a type alias, TypeScript will defer resolving type arguments to permit these patterns.
这意味着像下面这样的代码试图表示 JSON……
¥This means that code like the following that was trying to represent JSON…
ts
type Json = string | number | boolean | null | JsonObject | JsonArray;interface JsonObject {[property: string]: Json;}interface JsonArray extends Array<Json> {}
最终可以在没有辅助接口的情况下重写。
¥can finally be rewritten without helper interfaces.
ts
type Json =| string| number| boolean| null| { [property: string]: Json }| Json[];
此新放宽还允许我们在元组中递归引用类型别名。以下代码以前会出错,现在已转换为有效的 TypeScript 代码。
¥This new relaxation also lets us recursively reference type aliases in tuples as well. The following code which used to error is now valid TypeScript code.
ts
type VirtualNode = string | [string, { [key: string]: any }, ...VirtualNode[]];const myNode: VirtualNode = ["div",{ id: "parent" },["div", { id: "first-child" }, "I'm the first child"],["div", { id: "second-child" }, "I'm the second child"],];
更多信息,你可以查看 阅读原始拉取请求信息。
¥For more information, you can read up on the original pull request.
--declaration
和 --allowJs
¥--declaration
and --allowJs
TypeScript 中的 declaration
标志允许我们从 TypeScript 源文件(即 .ts
和 .tsx
文件)生成 .d.ts
文件(声明文件)。这些 .d.ts
文件非常重要,原因如下。
¥The declaration
flag in TypeScript allows us to generate .d.ts
files (declaration files) from TypeScript source files (i.e. .ts
and .tsx
files).
These .d.ts
files are important for a couple of reasons.
首先,它们很重要,因为它们允许 TypeScript 针对其他项目进行类型检查,而无需重新检查原始源代码。它们也很重要,因为它们允许 TypeScript 与现有的 JavaScript 库进行互操作,而这些库在构建时并非以 TypeScript 为核心。最后,还有一个经常被低估的好处:TypeScript 和 JavaScript 用户在使用 TypeScript 编辑器时都可以从这些文件中受益,例如获得更好的自动补全功能。
¥First of all, they’re important because they allow TypeScript to type-check against other projects without re-checking the original source code. They’re also important because they allow TypeScript to interoperate with existing JavaScript libraries that weren’t built with TypeScript in mind. Finally, a benefit that is often underappreciated: both TypeScript and JavaScript users can benefit from these files when using editors powered by TypeScript to get things like better auto-completion.
遗憾的是,declaration
不支持允许混合 TypeScript 和 JavaScript 输入文件的 allowJs
标志。这是一个令人沮丧的限制,因为这意味着用户在迁移代码库时无法使用 declaration
标志,即使它们带有 JSDoc 注释。TypeScript 3.7 改变了这一点,允许将这两个选项一起使用!
¥Unfortunately, declaration
didn’t work with the allowJs
flag which allows mixing TypeScript and JavaScript input files.
This was a frustrating limitation because it meant users couldn’t use the declaration
flag when migrating codebases, even if they were JSDoc-annotated.
TypeScript 3.7 changes that, and allows the two options to be used together!
此功能最有影响力的结果可能有些微妙:在 TypeScript 3.7 中,用户可以使用带 JSDoc 注释的 JavaScript 编写库,并支持 TypeScript 用户。
¥The most impactful outcome of this feature might a bit subtle: with TypeScript 3.7, users can write libraries in JSDoc annotated JavaScript and support TypeScript users.
其工作原理是,当使用 allowJs
时,TypeScript 会尽力进行一些分析,以理解常见的 JavaScript 模式;但是,某些模式在 JavaScript 中的表达方式不一定与 TypeScript 中的对应方式相同。当 declaration
触发启用时,TypeScript 会找出将 JSDoc 注释和 CommonJS 导出转换为输出 .d.ts
文件中有效类型声明等的最佳方法。
¥The way that this works is that when using allowJs
, TypeScript has some best-effort analyses to understand common JavaScript patterns; however, the way that some patterns are expressed in JavaScript don’t necessarily look like their equivalents in TypeScript.
When declaration
emit is turned on, TypeScript figures out the best way to transform JSDoc comments and CommonJS exports into valid type declarations and the like in the output .d.ts
files.
例如,以下代码片段
¥As an example, the following code snippet
js
const assert = require("assert");module.exports.blurImage = blurImage;/*** Produces a blurred image from an input buffer.* * @param input {Uint8Array}* @param width {number}* @param height {number}*/function blurImage(input, width, height) {const numPixels = width * height * 4;assert(input.length === numPixels);const result = new Uint8Array(numPixels);// TODOreturn result;}
将生成一个类似 .d.ts
的文件
¥Will produce a .d.ts
file like
ts
/*** Produces a blurred image from an input buffer.* * @param input {Uint8Array}* @param width {number}* @param height {number}*/export function blurImage(input: Uint8Array,width: number,height: number): Uint8Array;
这也可以超越带有 @param
标签的基本功能,例如:
¥This can go beyond basic functions with @param
tags too, where the following example:
js
/*** @callback Job* @returns {void}*//** Queues work */export class Worker {constructor(maxDepth = 10) {this.started = false;this.depthLimit = maxDepth;/*** NOTE: queued jobs may add more items to queue* @type {Job[]}*/this.queue = [];}/*** Adds a work item to the queue* @param {Job} work*/push(work) {if (this.queue.length + 1 > this.depthLimit) throw new Error("Queue full!");this.queue.push(work);}/*** Starts the queue if it has not yet started*/start() {if (this.started) return false;this.started = true;while (this.queue.length) {/** @type {Job} */ (this.queue.shift())();}return true;}}
将转换为以下 .d.ts
文件:
¥will be transformed into the following .d.ts
file:
ts
/*** @callback Job* @returns {void}*//** Queues work */export class Worker {constructor(maxDepth?: number);started: boolean;depthLimit: number;/*** NOTE: queued jobs may add more items to queue* @type {Job[]}*/queue: Job[];/*** Adds a work item to the queue* @param {Job} work*/push(work: Job): void;/*** Starts the queue if it has not yet started*/start(): boolean;}export type Job = () => void;
请注意,当同时使用这些标志时,TypeScript 不一定要降低 .js
文件的级别。如果你只是希望 TypeScript 创建 .d.ts
文件,则可以使用 emitDeclarationOnly
编译器选项。
¥Note that when using these flags together, TypeScript doesn’t necessarily have to downlevel .js
files.
If you simply want TypeScript to create .d.ts
files, you can use the emitDeclarationOnly
compiler option.
更多详情,请参阅 查看原始拉取请求。
¥For more details, you can check out the original pull request.
useDefineForClassFields
标志和 declare
属性修饰符
¥The useDefineForClassFields
Flag and The declare
Property Modifier
当 TypeScript 实现公共类字段时,我们尽力假设以下代码:
¥Back when TypeScript implemented public class fields, we assumed to the best of our abilities that the following code
ts
class C {foo = 100;bar: string;}
相当于在构造函数体内进行类似的赋值。
¥would be equivalent to a similar assignment within a constructor body.
ts
class C {constructor() {this.foo = 100;}}
遗憾的是,虽然这似乎是该提案早期的发展方向,但公共类字段极有可能以不同的方式进行标准化。相反,原始代码示例可能需要去糖化为更接近以下内容的内容:
¥Unfortunately, while this seemed to be the direction that the proposal moved towards in its earlier days, there is an extremely strong chance that public class fields will be standardized differently. Instead, the original code sample might need to de-sugar to something closer to the following:
ts
class C {constructor() {Object.defineProperty(this, "foo", {enumerable: true,configurable: true,writable: true,value: 100,});Object.defineProperty(this, "bar", {enumerable: true,configurable: true,writable: true,value: void 0,});}}
虽然 TypeScript 3.7 默认不会更改任何现有的输出,但我们一直在逐步推出更改,以帮助用户减少未来可能出现的错误。我们提供了一个名为 useDefineForClassFields
的新标志来启用此触发模式,并添加了一些新的检查逻辑。
¥While TypeScript 3.7 isn’t changing any existing emit by default, we’ve been rolling out changes incrementally to help users mitigate potential future breakage.
We’ve provided a new flag called useDefineForClassFields
to enable this emit mode with some new checking logic.
最大的两个变化如下:
¥The two biggest changes are the following:
-
声明使用
Object.defineProperty
初始化。¥Declarations are initialized with
Object.defineProperty
. -
即使没有初始化器,声明也始终初始化为
undefined
。¥Declarations are always initialized to
undefined
, even if they have no initializer.
这可能会对使用继承的现有代码造成相当大的影响。首先,来自基类的 set
访问器将不会被触发。 - 它们将被完全覆盖。
¥This can cause quite a bit of fallout for existing code that use inheritance. First of all, set
accessors from base classes won’t get triggered - they’ll be completely overwritten.
ts
class Base {set data(value: string) {console.log("data changed to " + value);}}class Derived extends Base {// No longer triggers a 'console.log'// when using 'useDefineForClassFields'.data = 10;}
其次,使用类字段来特化基类的属性也行不通。
¥Secondly, using class fields to specialize properties from base classes also won’t work.
ts
interface Animal {animalStuff: any;}interface Dog extends Animal {dogStuff: any;}class AnimalHouse {resident: Animal;constructor(animal: Animal) {this.resident = animal;}}class DogHouse extends AnimalHouse {// Initializes 'resident' to 'undefined'// after the call to 'super()' when// using 'useDefineForClassFields'!resident: Dog;constructor(dog: Dog) {super(dog);}}
归根结底,混合使用属性和访问器会导致问题,重新声明没有初始化器的属性也会导致问题。
¥What these two boil down to is that mixing properties with accessors is going to cause issues, and so will re-declaring properties with no initializers.
为了检测访问器相关的问题,TypeScript 3.7 现在将在 .d.ts
文件中触发 get
/set
访问器,以便 TypeScript 可以检查被覆盖的访问器。
¥To detect the issue around accessors, TypeScript 3.7 will now emit get
/set
accessors in .d.ts
files so that in TypeScript can check for overridden accessors.
受类字段更改影响的代码可以通过将字段初始化器转换为构造函数主体中的赋值来解决这个问题。
¥Code that’s impacted by the class fields change can get around the issue by converting field initializers to assignments in constructor bodies.
ts
class Base {set data(value: string) {console.log("data changed to " + value);}}class Derived extends Base {constructor() {this.data = 10;}}
为了缓解第二个问题,你可以添加显式初始化器或添加 declare
修饰符来指示属性不应触发任何值。
¥To help mitigate the second issue, you can either add an explicit initializer or add a declare
modifier to indicate that a property should have no emit.
ts
interface Animal {animalStuff: any;}interface Dog extends Animal {dogStuff: any;}class AnimalHouse {resident: Animal;constructor(animal: Animal) {this.resident = animal;}}class DogHouse extends AnimalHouse {declare resident: Dog;// ^^^^^^^// 'resident' now has a 'declare' modifier,// and won't produce any output code.constructor(dog: Dog) {super(dog);}}
目前,useDefineForClassFields
仅在 ES5 及以上版本可用,因为 ES3 中不存在 Object.defineProperty
。为了实现类似的问题检查,你可以创建一个单独的项目,该项目以 ES5 为目标并使用 noEmit
,以避免完整构建。
¥Currently useDefineForClassFields
is only available when targeting ES5 and upwards, since Object.defineProperty
doesn’t exist in ES3.
To achieve similar checking for issues, you can create a separate project that targets ES5 and uses noEmit
to avoid a full build.
更多信息,你可以查看 查看这些更改的原始拉取请求。
¥For more information, you can take a look at the original pull request for these changes.
我们强烈建议用户尝试使用 useDefineForClassFields
标记,并在我们的问题跟踪器或下方评论区进行反馈。这包括关于采用该标志的难度的反馈,以便我们了解如何简化迁移。
¥We strongly encourage users to try the useDefineForClassFields
flag and report back on our issue tracker or in the comments below.
This includes feedback on difficulty of adopting the flag so we can understand how we can make migration easier.
使用项目引用进行免编译编辑
¥Build-Free Editing with Project References
TypeScript 的项目引用为我们提供了一种轻松拆分代码库的方法,从而加快编译速度。遗憾的是,编辑依赖尚未构建(或输出已过期)的项目会导致编辑体验不佳。
¥TypeScript’s project references provide us with an easy way to break codebases up to give us faster compiles. Unfortunately, editing a project whose dependencies hadn’t been built (or whose output was out of date) meant that the editing experience wouldn’t work well.
在 TypeScript 3.7 中,当打开包含依赖的项目时,TypeScript 将自动使用源 .ts
/.tsx
文件。这意味着使用项目引用的项目现在将获得改进的编辑体验,其中语义操作是最新的并且符合 “正常工作” 标准。你可以使用编译器选项 disableSourceOfProjectReferenceRedirect
禁用此行为,这在处理非常大的项目时可能很合适,因为此更改可能会影响编辑性能。
¥In TypeScript 3.7, when opening a project with dependencies, TypeScript will automatically use the source .ts
/.tsx
files instead.
This means projects using project references will now see an improved editing experience where semantic operations are up-to-date and “just work”.
You can disable this behavior with the compiler option disableSourceOfProjectReferenceRedirect
which may be appropriate when working in very large projects where this change may impact editing performance.
¥You can read up more about this change by reading up on its pull request.
未调用函数检查
¥Uncalled Function Checks
一个常见且危险的错误是忘记调用函数,尤其是在函数没有参数或命名方式暗示它可能是一个属性而不是函数的情况下。
¥A common and dangerous error is to forget to invoke a function, especially if the function has zero arguments or is named in a way that implies it might be a property rather than a function.
ts
interface User {isAdministrator(): boolean;notify(): void;doNotDisturb?(): boolean;}// later...// Broken code, do not use!function doAdminThing(user: User) {// oops!if (user.isAdministrator) {sudo();editTheConfiguration();} else {throw new AccessDeniedError("User is not an admin");}}
这里,我们忘记调用 isAdministrator
,代码错误地允许非管理员用户编辑配置!
¥Here, we forgot to call isAdministrator
, and the code incorrectly allows non-administrator users to edit the configuration!
在 TypeScript 3.7 中,这被识别为可能的错误:
¥In TypeScript 3.7, this is identified as a likely error:
ts
function doAdminThing(user: User) {if (user.isAdministrator) {// ~~~~~~~~~~~~~~~~~~~~// error! This condition will always return true since the function is always defined.// Did you mean to call it instead?
此检查是一项重大更改,但因此检查非常保守。此错误仅在 if
条件下触发,对于可选属性、strictNullChecks
关闭或稍后在 if
主体内调用该函数时不会触发此错误:
¥This check is a breaking change, but for that reason the checks are very conservative.
This error is only issued in if
conditions, and it is not issued on optional properties, if strictNullChecks
is off, or if the function is later called within the body of the if
:
ts
interface User {isAdministrator(): boolean;notify(): void;doNotDisturb?(): boolean;}function issueNotification(user: User) {if (user.doNotDisturb) {// OK, property is optional}if (user.notify) {// OK, called the functionuser.notify();}}
如果你打算在不调用函数的情况下对其进行测试,则可以更正其定义以包含 undefined
/null
,或者使用 !!
编写类似 if (!!user.isAdministrator)
的代码来表明强制转换是故意的。
¥If you intended to test the function without calling it, you can correct the definition of it to include undefined
/null
, or use !!
to write something like if (!!user.isAdministrator)
to indicate that the coercion is intentional.
非常感谢 GitHub 用户 @jwbay,他主动创建了 proof-of-concept,并不断迭代,最终为我们提供了 当前版本。
¥We owe a big thanks to GitHub user @jwbay who took the initiative to create a proof-of-concept and iterated to provide us with the current version.
TypeScript 文件中的 // @ts-nocheck
¥// @ts-nocheck
in TypeScript Files
TypeScript 3.7 允许我们在 TypeScript 文件顶部添加 // @ts-nocheck
注释以禁用语义检查。以前,只有在 JavaScript 源文件中存在 checkJs
的情况下,此注释才会被遵守,但我们已扩展对 TypeScript 文件的支持,以便所有用户更轻松地进行迁移。
¥TypeScript 3.7 allows us to add // @ts-nocheck
comments to the top of TypeScript files to disable semantic checks.
Historically this comment was only respected in JavaScript source files in the presence of checkJs
, but we’ve expanded support to TypeScript files to make migrations easier for all users.
分号格式化程序选项
¥Semicolon Formatter Option
由于 JavaScript 的自动分号插入 (ASI) 规则,TypeScript 的内置格式化程序现在支持在尾随分号可选的位置插入和删除分号。该设置现已在 Visual Studio Code 内部人员 中可用,并将在 Visual Studio 16.4 Preview 2 的“工具选项”菜单中提供。
¥TypeScript’s built-in formatter now supports semicolon insertion and removal at locations where a trailing semicolon is optional due to JavaScript’s automatic semicolon insertion (ASI) rules. The setting is available now in Visual Studio Code Insiders, and will be available in Visual Studio 16.4 Preview 2 in the Tools Options menu.

选择 “insert” 或 “remove” 的值也会影响自动导入、提取类型以及 TypeScript 服务提供的其他生成代码的格式。保留其默认值 “ignore”,生成的代码将与当前文件中检测到的分号首选项匹配。
¥Choosing a value of “insert” or “remove” also affects the format of auto-imports, extracted types, and other generated code provided by TypeScript services. Leaving the setting on its default value of “ignore” makes generated code match the semicolon preference detected in the current file.
3.7 重大变更
¥3.7 Breaking Changes
DOM 变更
¥DOM Changes
lib.dom.d.ts
中的类型已更新。这些更改主要是为了解决与可空性相关的正确性问题,但最终的影响取决于你的代码库。
¥Types in lib.dom.d.ts
have been updated.
These changes are largely correctness changes related to nullability, but impact will ultimately depend on your codebase.
类字段缓解措施
¥Class Field Mitigations
如上所述,TypeScript 3.7 在 .d.ts
文件中触发 get
/set
访问器,这可能会对使用旧版本 TypeScript(例如 3.5 及之前版本)的用户造成重大更改。TypeScript 3.6 用户不会受到影响,因为该版本已为此功能做好了未来准备。
¥As mentioned above, TypeScript 3.7 emits get
/set
accessors in .d.ts
files which can cause breaking changes for consumers on older versions of TypeScript like 3.5 and prior.
TypeScript 3.6 users will not be impacted, since that version was future-proofed for this feature.
虽然本质上并非破坏性变更,但在以下情况下选择启用 useDefineForClassFields
标志可能会导致破坏性变更:
¥While not a breakage per se, opting in to the useDefineForClassFields
flag can cause breakage when:
-
使用属性声明覆盖派生类中的访问器
¥overriding an accessor in a derived class with a property declaration
-
重新声明没有初始化器的属性声明
¥re-declaring a property declaration with no initializer
要了解全部影响,请阅读 上面关于 useDefineForClassFields
标志的部分。
¥To understand the full impact, read the section above on the useDefineForClassFields
flag.
函数真值检查
¥Function Truthy Checks
如上所述,当 if
语句条件中出现函数未调用的情况时,TypeScript 现在会报错。在 if
条件下检查函数类型时会出错,除非满足以下任一条件:
¥As mentioned above, TypeScript now errors when functions appear to be uncalled within if
statement conditions.
An error is issued when a function type is checked in if
conditions unless any of the following apply:
-
被检查的值来自可选属性。
¥the checked value comes from an optional property
-
strictNullChecks
已禁用¥
strictNullChecks
is disabled -
该函数随后在
if
的主函数中被调用。¥the function is later called within the body of the
if
本地和导入类型声明现在冲突
¥Local and Imported Type Declarations Now Conflict
由于一个 bug,TypeScript 之前允许使用以下构造:
¥Due to a bug, the following construct was previously allowed in TypeScript:
ts
// ./someOtherModule.tsinterface SomeType {y: string;}// ./myModule.tsimport { SomeType } from "./someOtherModule";export interface SomeType {x: number;}function fn(arg: SomeType) {console.log(arg.x); // Error! 'x' doesn't exist on 'SomeType'}
这里,SomeType
似乎同时源于 import
声明和本地 interface
声明。可能令人惊讶的是,在模块内部,SomeType
仅引用 import
定义,而本地声明 SomeType
仅在从其他文件导入时可用。这非常令人困惑,我们对极少数此类代码案例的审查表明,开发者通常认为发生了其他事情。
¥Here, SomeType
appears to originate in both the import
declaration and the local interface
declaration.
Perhaps surprisingly, inside the module, SomeType
refers exclusively to the import
ed definition, and the local declaration SomeType
is only usable when imported from another file.
This is very confusing and our review of the very small number of cases of code like this in the wild showed that developers usually thought something different was happening.
在 TypeScript 3.7 中,现在可正确识别为重复标识符错误。正确的修复方法取决于作者的初衷,应根据具体情况进行处理。通常,命名冲突是无意的,最好的解决方法是重命名导入的类型。如果目的是扩充导入的类型,则应编写适当的模块扩充方法。
¥In TypeScript 3.7, this is now correctly identified as a duplicate identifier error. The correct fix depends on the original intent of the author and should be addressed on a case-by-case basis. Usually, the naming conflict is unintentional and the best fix is to rename the imported type. If the intent was to augment the imported type, a proper module augmentation should be written instead.
3.7 API 变更
¥3.7 API Changes
为了启用上面描述的递归类型别名模式,typeArguments
属性已从 TypeReference
接口中移除。用户应该在 TypeChecker
实例上使用 getTypeArguments
函数。
¥To enable the recursive type alias patterns described above, the typeArguments
property has been removed from the TypeReference
interface. Users should instead use the getTypeArguments
function on TypeChecker
instances.