TypeScript 4.1

模板字面类型

¥Template Literal Types

TypeScript 中的字符串字面量类型允许我们建模需要一组特定字符串的函数和 API。

¥String literal types in TypeScript allow us to model functions and APIs that expect a set of specific strings.

ts
function setVerticalAlignment(location: "top" | "middle" | "bottom") {
// ...
}
 
setVerticalAlignment("middel");
Argument of type '"middel"' is not assignable to parameter of type '"top" | "middle" | "bottom"'.2345Argument of type '"middel"' is not assignable to parameter of type '"top" | "middle" | "bottom"'.
Try

这非常好,因为字符串字面量类型基本上可以对我们的字符串值进行拼写检查。

¥This is pretty nice because string literal types can basically spell-check our string values.

我们也喜欢字符串字面量可以用作映射类型中的属性名。从这个意义上讲,它们也可以用作构建块:

¥We also like that string literals can be used as property names in mapped types. In this sense, they’re also usable as building blocks:

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

但是字符串字面量类型还可以用作构建块:构建其他字符串字面量类型。

¥But there’s another place that string literal types could be used as building blocks: building other string literal types.

这就是为什么 TypeScript 4.1 引入了模板字符串类型。它的语法与 JavaScript 中的模板字面字符串 相同,但用于类型位置。当你将其与具体字面量类型一起使用时,它会通过连接内容生成一个新的字符串字面量类型。

¥That’s why TypeScript 4.1 brings the template literal string type. It has the same syntax as template literal strings in JavaScript, but is used in type positions. When you use it with concrete literal types, it produces a new string literal type by concatenating the contents.

ts
type World = "world";
 
type Greeting = `hello ${World}`;
type Greeting = "hello world"
Try

如果替换位置有联合,会发生什么?它生成每个联合成员可能表示的所有字符串字面量的集合。

¥What happens when you have unions in substitution positions? It produces the set of every possible string literal that could be represented by each union member.

ts
type Color = "red" | "blue";
type Quantity = "one" | "two";
 
type SeussFish = `${Quantity | Color} fish`;
type SeussFish = "one fish" | "two fish" | "red fish" | "blue fish"
Try

除了发行说明中那些可爱的示例之外,它还可以用于其他用途。例如,一些 UI 组件库在其 API 中提供了同时指定垂直和水平对齐的方式,通常使用像 "bottom-right" 这样的单个字符串同时指定两者。在垂直对齐 "top""middle""bottom" 之间,以及水平对齐 "left""center""right" 之间,有 9 种可能的字符串,其中每个前一个字符串都使用短划线与每个后一个字符串连接。

¥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.

ts
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!
Argument of type '"top-middel"' is not assignable to parameter of type '"top-left" | "top-center" | "top-right" | "middle-left" | "middle-center" | "middle-right" | "bottom-left" | "bottom-center" | "bottom-right"'.2345Argument of type '"top-middel"' is not assignable to parameter of type '"top-left" | "top-center" | "top-right" | "middle-left" | "middle-center" | "middle-right" | "bottom-left" | "bottom-center" | "bottom-right"'.
setAlignment("top-pot"); // error! but good doughnuts if you're ever in Seattle
Argument of type '"top-pot"' is not assignable to parameter of type '"top-left" | "top-center" | "top-right" | "middle-left" | "middle-center" | "middle-right" | "bottom-left" | "bottom-center" | "bottom-right"'.2345Argument of type '"top-pot"' is not assignable to parameter of type '"top-left" | "top-center" | "top-right" | "middle-left" | "middle-center" | "middle-right" | "bottom-left" | "bottom-center" | "bottom-right"'.
Try

虽然这类 API 的例子很多,但这仍然只是一个玩具示例,因为我们可以手动编写它们。事实上,对于 9 个字符串来说,这可能没问题;但是当你需要大量字符串时,你应该考虑提前自动生成它们,以节省每次类型检查的工作(或者直接使用 string,这会更容易理解)。

¥While there are lots of examples of this sort of API in the wild, this is still a bit of a toy example since we could write these out manually. In fact, for 9 strings, this is likely fine; but when you need a ton of strings, you should consider automatically generating them ahead of time to save work on every type-check (or just use string, which will be much simpler to comprehend).

一些实际价值在于动态创建新的字符串字面量。例如,假设一个 makeWatchedObject API 接受一个对象并生成一个几乎相同的对象,但使用一个新的 on 方法来检测属性的变化。

¥Some of the real value comes from dynamically creating new string literals. For example, imagine a makeWatchedObject API that takes an object and produces a mostly identical object, but with a new on method to detect for changes to the properties.

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

请注意,on 监听事件 "firstNameChanged",而不仅仅是 "firstName"。我们如何为这段代码编写类型?

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

ts
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>;

这样,我们可以构建一些在提供错误属性时会出错的东西!

¥With this, we can build something that errors when we give the wrong property!

ts
// error!
person.on("firstName", () => {});
Argument of type '"firstName"' is not assignable to parameter of type '"firstNameChanged" | "ageChanged" | "locationChanged"'.2345Argument of type '"firstName"' is not assignable to parameter of type '"firstNameChanged" | "ageChanged" | "locationChanged"'.
 
// error!
person.on("frstNameChanged", () => {});
Argument of type '"frstNameChanged"' is not assignable to parameter of type '"firstNameChanged" | "ageChanged" | "locationChanged"'.2345Argument of type '"frstNameChanged"' is not assignable to parameter of type '"firstNameChanged" | "ageChanged" | "locationChanged"'.
Try

我们还可以在模板字面量类型中做一些特殊的事情:我们可以从替换位置推断。我们可以使最后一个例子具有泛型,以便从 eventName 字符串的各个部分推断出关联的属性。

¥We can also do something special in template literal types: we can infer from substitution positions. We can make our last example generic to infer from parts of the eventName string to figure out the associated property.

ts
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");
}
})
Try

这里我们把 on 变成了一个泛型方法。当用户使用字符串 "firstNameChanged' 调用时,TypeScript 将尝试推断 K 的正确类型。为此,它会将 K"Changed" 之前的内容进行匹配,并推断出字符串 "firstName"。一旦 TypeScript 确定了这一点,on 方法就可以在原始对象上获取 firstName 的类型,在本例中为 string。同样,当我们使用 "ageChanged" 调用时,它会找到属性 age 的类型,即 number)。

¥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).

推断可以以不同的方式组合,通常是解构字符串,并以不同的方式重构它们。事实上,为了帮助修改这些字符串字面量类型,我们添加了一些新的工具类型别名,用于修改字母的大小写(即转换为小写和大写字符)。

¥Inference can be combined in different ways, often to deconstruct strings, and reconstruct them in different ways. In fact, to help with modifying these string literal types, we’ve added a few new utility type aliases for modifying casing in letters (i.e. converting to lowercase and uppercase characters).

ts
type EnthusiasticGreeting<T extends string> = `${Uppercase<T>}`
 
type HELLO = EnthusiasticGreeting<"hello">;
type HELLO = "HELLO"
Try

新的类型别名是 UppercaseLowercaseCapitalizeUncapitalize。前两种方法会转换字符串中的每个字符,后两种方法只转换字符串中的第一个字符。

¥The new type aliases are Uppercase, Lowercase, Capitalize and Uncapitalize. The first two transform every character in a string, and the latter two transform only the first character in a string.

更多详情,请访问 查看原始拉取请求正在进行的切换到类型别名的拉取请求辅助函数

¥For more details, see the original pull request and the in-progress pull request to switch to type alias helpers.

映射类型中的键重映射

¥Key Remapping in Mapped Types

提醒一下,映射类型可以基于任意键创建新的对象类型。

¥Just as a refresher, a mapped type can create new object types based on arbitrary keys

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

或者基于其他对象类型创建新的对象类型。

¥or new object types based on other object types.

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

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

¥Until now, mapped types could only produce new object types with keys that you provided them; however, lots of the time you want to be able to create new keys, or filter out keys, based on the inputs.

这就是为什么 TypeScript 4.1 允许你使用新的 as 子句重新映射映射类型中的键。

¥That’s why TypeScript 4.1 allows you to re-map keys in mapped types with a new as clause.

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

借助这个新的 as 子句,你可以利用模板字面量类型等功能,轻松地基于旧属性名创建属性名。

¥With this new as clause, you can leverage features like template literal types to easily create property names based off of old ones.

ts
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>;
type LazyPerson = { getName: () => string; getAge: () => number; getLocation: () => string; }
Try

你甚至可以通过生成 never 来过滤掉键。这意味着在某些情况下,你不必使用额外的 Omit 辅助类型。

¥and you can even filter out keys by producing never. That means you don’t have to use an extra Omit helper type in some cases.

ts
// 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>;
type KindlessCircle = { radius: number; }
Try

更多信息,请查看 GitHub 上的原始拉取请求

¥For more information, take a look at the original pull request over on GitHub.

递归条件类型

¥Recursive Conditional Types

在 JavaScript 中,函数可以展平并在任意级别构建容器类型的情况相当普遍。例如,考虑 Promise 实例上的 .then() 方法。.then(...) 解开每个 Promise,直到找到一个非 “promise-like” 的值,并将该值传递给回调。Array 上还有一个相对较新的 flat 方法,可以接受深度来决定展平的深度。

¥In JavaScript it’s fairly common to see functions that can flatten and build up container types at arbitrary levels. For example, consider the .then() method on instances of Promise. .then(...) unwraps each promise until it finds a value that’s not “promise-like”, and passes that value to a callback. There’s also a relatively new flat method on Arrays that can take a depth of how deep to flatten.

在 TypeScript 的类型系统中表达这一点,实际上是不可能的。虽然有一些技巧可以实现这一点,但最终的类型看起来非常不合理。

¥Expressing this in TypeScript’s type system was, for all practical intents and purposes, not possible. While there were hacks to achieve this, the types ended up looking very unreasonable.

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

¥That’s why TypeScript 4.1 eases some restrictions on conditional types - so that they can model these patterns. In TypeScript 4.1, conditional types can now immediately reference themselves within their branches, making it easier to write recursive type aliases.

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

¥For example, if we wanted to write a type to get the element types of nested arrays, we could write the following deepFlatten type.

ts
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]]]]);

同样,在 TypeScript 4.1 中,我们可以编写一个 Awaited 类型来深度解包 Promise

¥Similarly, in TypeScript 4.1 we can write an Awaited type to deeply unwrap Promises.

ts
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>>;

请记住,虽然这些递归类型功能强大,但应谨慎使用。

¥Keep in mind that while these recursive types are powerful, they should be used responsibly and sparingly.

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

¥First off, these types can do a lot of work which means that they can increase type-checking time. Trying to model numbers in the Collatz conjecture or Fibonacci sequence might be fun, but don’t ship that in .d.ts files on npm.

但除了计算密集型之外,这些类型在足够复杂的输入下可能会达到内部递归深度限制。当达到递归限制时,会导致编译时错误。一般来说,最好完全不要使用这些类型,以免在实际示例中编写失败的代码。

¥But apart from being computationally intensive, these types can hit an internal recursion depth limit on sufficiently-complex inputs. When that recursion limit is hit, that results in a compile-time error. In general, it’s better not to use these types at all than to write something that fails on more realistic examples.

查看更多 在实现时

¥See more at the implementation.

已检查的索引访问 (--noUncheckedIndexedAccess)

¥Checked Indexed Accesses (--noUncheckedIndexedAccess)

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

¥TypeScript has a feature called index signatures. These signatures are a way to signal to the type system that users can access arbitrarily-named properties.

ts
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();
}
Try

在上面的例子中,Options 有一个索引签名,表示任何未列出的访问属性都应该具有类型 string | number。这通常对于乐观代码来说很方便,因为它假设你知道自己在做什么,但事实上,JavaScript 中的大多数值并不支持所有潜在的属性名称。例如,大多数类型不会像上例中那样,为 Math.random() 创建的属性键赋予值。对于许多用户来说,这种行为并不受欢迎,感觉它没有充分利用 strictNullChecks 的严格检查机制。

¥In the above example, Options has an index signature that says any accessed property that’s not already listed should have the type string | number. This is often convenient for optimistic code that assumes you know what you’re doing, but the truth is that most values in JavaScript do not support every potential property name. Most types will not, for example, have a value for a property key created by Math.random() like in the previous example. For many users, this behavior was undesirable, and felt like it wasn’t leveraging the full strict-checking of strictNullChecks.

这就是为什么 TypeScript 4.1 附带了一个名为 noUncheckedIndexedAccess 的新标志。在此新模式下,每个属性访问(如 foo.bar)或索引访问(如 foo["bar"])都被视为潜在未定义。这意味着在我们上一个示例中,opts.yadda 将具有类型 string | number | undefined,而不仅仅是 string | number。如果你需要访问该属性,则必须先检查其是否存在,或者使用非空断言运算符(后缀 ! 字符)。

¥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).

ts
function checkOptions(opts: Options) {
opts.path; // string
opts.permissions; // number
 
// These are not allowed with noUncheckedIndexedAccess
opts.yadda.toString();
'opts.yadda' is possibly 'undefined'.18048'opts.yadda' is possibly 'undefined'.
opts["foo bar baz"].toString();
Object is possibly 'undefined'.2532Object is possibly 'undefined'.
opts[Math.random()].toString();
Object is possibly 'undefined'.2532Object is possibly 'undefined'.
 
// 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();
}
Try

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

¥One consequence of using noUncheckedIndexedAccess is that indexing into an array is also more strictly checked, even in a bounds-checked loop.

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

如果你不需要索引,可以使用 for-of 循环或 forEach 调用来迭代单个元素。

¥If you don’t need the indexes, you can iterate over individual elements by using a for-of loop or a forEach call.

ts
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());
});
}
Try

此标志对于捕获越界错误非常有用,但对于很多代码来说可能会很嘈杂,因此它不会被 strict 标志自动启用;但是,如果你对此功能感兴趣,请随意尝试,看看它是否适合你团队的代码库!

¥This flag can be handy for catching out-of-bounds errors, but it might be noisy for a lot of code, so it is not automatically enabled by the strict flag; however, if this feature is interesting to you, you should feel free to try it and determine whether it makes sense for your team’s codebase!

你可以了解更多有关 在实现拉取请求时 的信息。

¥You can learn more at the implementing pull request.

不带 baseUrlpaths

¥paths without baseUrl

使用路径映射相当常见 - 通常是为了更好的导入,也是为了模拟 monorepo 链接行为。

¥Using path-mapping is fairly common - often it’s to have nicer imports, often it’s to simulate monorepo linking behavior.

遗憾的是,指定 paths 来启用路径映射还需要指定一个名为 baseUrl 的选项,该选项也允许相对于 baseUrl 访问裸说明符路径。这也经常导致自动导入使用错误的路径。

¥Unfortunately, specifying paths to enable path-mapping required also specifying an option called baseUrl, which allows bare specifier paths to be reached relative to the baseUrl too. This also often caused poor paths to be used by auto-imports.

在 TypeScript 4.1 中,paths 选项可以独立于 baseUrl 使用。这有助于避免其中一些问题。

¥In TypeScript 4.1, the paths option can be used without baseUrl. This helps avoid some of these issues.

checkJs 隐含 allowJs

¥checkJs Implies allowJs

之前,如果你要启动一个已检查的 JavaScript 项目,则必须同时设置 allowJscheckJs。这在体验中是一个稍微令人讨厌的摩擦,所以 checkJs 现在默认意味着 allowJs

¥Previously if you were starting a checked JavaScript project, you had to set both allowJs and checkJs. This was a slightly annoying bit of friction in the experience, so checkJs now implies allowJs by default.

查看拉取请求中的更多详细信息

¥See more details at the pull request.

React 17 JSX 工厂函数

¥React 17 JSX Factories

TypeScript 4.1 通过 jsx 编译器选项的两个新选项支持 React 17 即将推出的 jsxjsxs 工厂函数:

¥TypeScript 4.1 supports React 17’s upcoming jsx and jsxs factory functions through two new options for the jsx compiler option:

  • react-jsx

  • react-jsxdev

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

¥These options are intended for production and development compiles respectively. Often, the options from one can extend from the other. For example, a tsconfig.json for production builds might look like the following:

// ./src/tsconfig.json
{
"": "esnext",
"": "es2015",
"": "react-jsx",
"": true
},
"": ["./**/*"]
}

用于开发构建的版本可能如下所示:

¥and one for development builds might look like the following:

// ./src/tsconfig.dev.json
{
"": "./tsconfig.json",
"": "react-jsxdev"
}
}

更多信息请见 查看相应的 PR

¥For more information, check out the corresponding PR.

编辑器支持 JSDoc @see 标签

¥Editor Support for the JSDoc @see Tag

JSDoc 标签 @see 现在在 TypeScript 和 JavaScript 编辑器中得到了更好的支持。这允许你在标签后的带点名称中使用诸如跳转到定义之类的功能。例如,在 JSDoc 注释中转到 firstC 的定义仅在以下示例中有效:

¥The JSDoc tag @see tag now has better support in editors for TypeScript and JavaScript. This allows you to use functionality like go-to-definition in a dotted name following the tag. For example, going to definition on first or C in the JSDoc comment just works in the following example:

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

感谢频繁贡献者 Wenlu Wang 用于实现此功能

¥Thanks to frequent contributor Wenlu Wang for implementing this!

打破变更

¥Breaking Changes

lib.d.ts 变更

¥lib.d.ts Changes

lib.d.ts 可能有一组 API 发生变化,这可能部分归因于 DOM 类型的自动生成方式。一项具体的变化是,Reflect.enumerate 已被移除,因为它已从 ES2016 中移除。

¥lib.d.ts may have a set of changed APIs, potentially in part due to how the DOM types are automatically generated. One specific change is that Reflect.enumerate has been removed, as it was removed from ES2016.

abstract 成员不能标记为 async

¥abstract Members Can’t Be Marked async

标记为 abstract 的成员不能再标记为 async。这里的修复方法是删除 async 关键字,因为调用者只关心返回类型。

¥Members marked as abstract can no longer be marked as async. The fix here is to remove the async keyword, since callers are only concerned with the return type.

any/unknown 在 Falsy 位置传递

¥any/unknown Are Propagated in Falsy Positions

以前,对于像 foo && somethingElse 这样的表达式,foo 的类型是 anyunknown,则整个表达式的类型将是 somethingElse 的类型。

¥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.

例如,之前这里 x 的类型是 { someProp: string }

¥For example, previously the type for x here was { someProp: string }.

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

但是,在 TypeScript 4.1 中,我们对如何确定这种类型更加谨慎。由于对 && 左侧的类型一无所知,因此我们向外传播 anyunknown,而不是右侧的类型。

¥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.

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

¥The most common pattern we saw of this tended to be when checking compatibility with booleans, especially in predicate functions.

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

通常,合适的解决方法是从 foo && someExpression 切换到 !!foo && someExpression

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

Promise 中,resolve 的参数不再是可选的

¥resolve’s Parameters Are No Longer Optional in Promises

编写如下代码时

¥When writing code like the following

ts
new Promise((resolve) => {
doSomethingAsync(() => {
doSomething();
resolve();
});
});

你可能会收到类似以下错误:

¥You may get an error like the following:

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

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

¥This is because resolve no longer has an optional parameter, so by default, it must now be passed a value. Often this catches legitimate bugs with using Promises. The typical fix is to pass it the correct argument, and sometimes to add an explicit type argument.

ts
new Promise<number>((resolve) => {
// ^^^^^^^^
doSomethingAsync((value) => {
doSomething();
resolve(value);
// ^^^^^
});
});

但是,有时 resolve() 确实需要在没有参数的情况下调用。在这些情况下,我们可以为 Promise 提供一个显式的 void 泛型类型参数(即将其写为 Promise<void>)。这利用了 TypeScript 4.1 中的新功能,其中潜在的 void 尾随参数可以变为可选参数。

¥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.

ts
new Promise<void>((resolve) => {
// ^^^^^^
doSomethingAsync(() => {
doSomething();
resolve();
});
});

TypeScript 4.1 附带一个快速修复程序来帮助修复此问题。

¥TypeScript 4.1 ships with a quick fix to help fix this break.

条件扩展创建可选属性

¥Conditional Spreads Create Optional Properties

在 JavaScript 中,对象展开(如 { ...foo })不会对假值进行操作。因此,在像 { ...foo } 这样的代码中,如果 foonullundefined,则会跳过它。

¥In JavaScript, object spreads (like { ...foo }) don’t operate over falsy values. So in code like { ...foo }, foo will be skipped over if it’s null or undefined.

许多用户利用这一点来扩展 “conditionally” 属性。

¥Many users take advantage of this to spread properties “conditionally”.

ts
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 的属性将会传播到 - 否则,返回的对象中将不会包含任何属性。

¥Here, if pet is defined, the properties of pet.owner will be spread in - otherwise, no properties will be spread into the returned object.

copyOwner 的返回类型以前是基于每个展开的联合类型:

¥The return type of copyOwner was previously a union type based on each spread:

{ x: number } | { x: number, name: string, age: number, location: string }

这精确地模拟了操作的发生方式:如果定义了 pet,则 Person 中的所有属性都将存在;否则,返回的结果中将不会定义任何属性。这是一个全有或全无的操作。

¥This modeled exactly how the operation would occur: if pet was defined, all the properties from Person would be present; otherwise, none of them would be defined on the result. It was an all-or-nothing operation.

但是,我们已经看到这种模式发展到了极端,单个对象中有数百个展开,每个展开都可能添加数百或数千个属性。事实证明,由于各种原因,这最终会非常昂贵,而且通常没有太多好处。

¥However, we’ve seen this pattern taken to the extreme, with hundreds of spreads in a single object, each spread potentially adding in hundreds or thousands of properties. It turns out that for various reasons, this ends up being extremely expensive, and usually for not much benefit.

在 TypeScript 4.1 中,返回的类型有时会使用全可选属性。

¥In TypeScript 4.1, the returned type sometimes uses all-optional properties.

{
x: number;
name?: string;
age?: number;
location?: string;
}

这最终会提高性能,并且通常显示效果也更好。

¥This ends up performing better and generally displaying better too.

详情请见 查看原始变更。虽然这种行为目前并不完全一致,但我们预计未来的版本将产生更清晰、更可预测的结果。

¥For more details, see the original change. While this behavior is not entirely consistent right now, we expect a future release will produce cleaner and more predictable results.

不匹配的参数不再相关

¥Unmatched parameters are no longer related

TypeScript 以前会通过将彼此不对应的参数与类型 any 关联来关联它们。使用 TypeScript 4.1 中的更改,该语言现在完全跳过了此过程。这意味着某些可赋值情况现在会失败,但也意味着某些重载解析情况也会失败。例如,在 Node.js 中对 util.promisify 函数的重载解析可能会在 TypeScript 4.1 中选择不同的重载,有时会导致下游出现新的或不同的错误。

¥TypeScript would previously relate parameters that didn’t correspond to each other by relating them to the type any. With changes in TypeScript 4.1, the language now skips this process entirely. This means that some cases of assignability will now fail, but it also means that some cases of overload resolution can fail as well. For example, overload resolution on util.promisify in Node.js may select a different overload in TypeScript 4.1, sometimes causing new or different errors downstream.

作为一种解决方法,你最好使用类型断言来消除错误。

¥As a workaround, you may be best using a type assertion to squelch errors.