Skip to content
虚位以待
虚位以待
虚位以待
虚位以待
虚位以待
虚位以待
虚位以待
虚位以待
虚位以待
虚位以待
虚位以待
虚位以待
虚位以待
虚位以待
虚位以待
虚位以待
虚位以待
虚位以待

TypeScript 4.1

模板字面量类型

TypeScript 中的字符串字面量类型允许我们对期望一组特定字符串的函数和 API 进行建模。

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

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

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

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

但还有另一个地方可以将字符串字面量类型用作构建块:构建其他字符串字面量类型。

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

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

当你在替换位置有联合类型时会发生什么? 它会生成每个联合成员可以表示的每个可能字符串字面量的集合。

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

这可以在发布说明中的可爱示例之外使用。 例如,一些 UI 组件库在其 API 中有一种指定垂直和水平对齐的方式,通常使用单个字符串(如 "bottom-right")同时指定两者。 在垂直对齐使用 "top""middle""bottom",水平对齐使用 "left""center""right" 的情况下,有 9 种可能的字符串,其中每个前者的字符串通过破折号与每个后者的字符串连接。

ts
type 
VerticalAlignment
= "top" | "middle" | "bottom";
type
HorizontalAlignment
= "left" | "center" | "right";
// 接受 // | "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"); // 有效!
setAlignment
("top-middel"); // 错误!
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"'.
setAlignment
("top-pot"); // 错误!但如果你在西雅图,这是个好甜甜圈
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"'.
Try

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

一些真正的价值来自于动态创建新的字符串字面量。 例如,想象一个 makeWatchedObject API,它接受一个对象并产生一个几乎相同的对象,但有一个新的 on 方法来检测属性的更改。

ts
let person = makeWatchedObject({
  firstName: "Homer",
  age: 42, // 大约
  location: "Springfield",
});

person.on("firstNameChanged", () => {
  console.log(`firstName was changed!`);
});

注意 on 监听的是 "firstNameChanged" 事件,而不仅仅是 "firstName"。 我们该如何为此类型化?

ts
type PropEventSource<T> = {
    on(eventName: `${string & keyof T}Changed`, callback: () => void): void;
};

/// 创建一个带有 'on' 方法的“被观察对象”
/// 以便你可以观察属性的变化。
declare function makeWatchedObject<T>(obj: T): T & PropEventSource<T>;

有了这个,我们可以在给出错误属性时构建一个会报错的东西!

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

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

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",
}); // 有效!'newName' 的类型为 'string'
person
.
on
("firstNameChanged",
newName
=> {
// 'newName' 具有 'firstName' 的类型
console
.
log
(`new name is ${
newName
.
toUpperCase
()}`);
}); // 有效!'newAge' 的类型为 '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

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

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

新的类型别名是 UppercaseLowercaseCapitalizeUncapitalize。 前两个转换字符串中的每个字符,后两个仅转换字符串中的第一个字符。

有关更多详细信息,请参阅原始拉取请求转换为类型别名助手的进行中拉取请求

映射类型中的键重映射

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

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

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

ts
/// 'Partial<T>' 与 'T' 相同,但每个属性都标记为可选。
type Partial<T> = {
  [K in keyof T]?: T[K];
};

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

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

ts
type MappedTypeWithNewKeys<T> = {
    [K in keyof T as NewKeyType]: T[K]
    //            ^^^^^^^^^^^^^
    //            这是新语法!
}

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

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

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

ts
// 移除 'kind' 属性
type 
RemoveKindField
<
T
> = {
[
K
in keyof
T
as
Exclude
<
K
, "kind">]:
T
[
K
]
}; interface Circle {
kind
: "circle";
radius
: number;
} type
KindlessCircle
=
RemoveKindField
<Circle>;
Try

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

递归条件类型

在 JavaScript 中,看到可以展平和构建任意深度的容器类型的函数是很常见的。 例如,考虑 Promise 实例上的 .then() 方法。 .then(...) 会展开每个 promise,直到找到一个不是“类 promise”的值,并将该值传递给回调。 还有一个相对较新的 Array 上的 flat 方法,它可以接受一个深度参数来指定展平深度。

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

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

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

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";
}

// 所有这些都返回类型 'number[]':
deepFlatten([1, 2, 3]);
deepFlatten([[1], [2, 3]]);
deepFlatten([[1], [[2]], [[[3]]]]);

类似地,在 TypeScript 4.1 中,我们可以编写一个 Awaited 类型来深度展开 Promise

ts
type Awaited<T> = T extends PromiseLike<infer U> ? Awaited<U> : T;

/// 就像 `promise.then(...)`,但类型更准确。
declare function customThen<T, U>(
  p: Promise<T>,
  onFulfilled: (value: Awaited<T>) => U
): Promise<Awaited<U>>;

请记住,虽然这些递归类型很强大,但应该负责任地、谨慎地使用它们。

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

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

查看更多实现细节

检查索引访问 (--noUncheckedIndexedAccess)

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

ts
interface Options {
  
path
: string;
permissions
: number;
// 额外属性由此索引签名捕获。 [
propName
: string]: string | number;
} function
checkOptions
(
opts
: Options) {
opts
.
path
; // string
opts
.
permissions
; // number
// 这些也都是允许的! // 它们具有类型 'string | number'。
opts
.
yadda
.
toString
();
opts
["foo bar baz"].
toString
();
opts
[
Math
.
random
()].
toString
();
}
Try

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

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

ts
function 
checkOptions
(
opts
: Options) {
opts
.
path
; // string
opts
.
permissions
; // number
// 使用 noUncheckedIndexedAccess 时不允许这些 opts.yadda.
toString
();
'opts.yadda' is possibly 'undefined'.
opts["foo bar baz"].
toString
();
Object is possibly 'undefined'.
opts[Math.random()].
toString
();
Object is possibly 'undefined'.
// 先检查它是否真的存在。 if (
opts
.
yadda
) {
console
.
log
(
opts
.
yadda
.
toString
());
} // 基本上说“相信我,我知道我在做什么” // 使用 '!' 非空断言运算符。
opts
.
yadda
!.
toString
();
}
Try

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

ts
function 
screamLines
(
strs
: string[]) {
// 这会有问题 for (let
i
= 0;
i
<
strs
.
length
;
i
++) {
console
.
log
(strs[i].
toUpperCase
());
Object is possibly 'undefined'.
} }
Try

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

ts
function 
screamLines
(
strs
: string[]) {
// 这正常工作 for (const
str
of
strs
) {
console
.
log
(
str
.
toUpperCase
());
} // 这正常工作
strs
.
forEach
((
str
) => {
console
.
log
(
str
.
toUpperCase
());
}); }
Try

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

你可以在实现拉取请求中了解更多信息。

没有 baseUrlpaths

使用路径映射相当常见——通常是为了获得更好的导入,或者模拟 monorepo 链接行为。

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

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

checkJs 隐含 allowJs

以前,如果你开始一个受检查的 JavaScript 项目,你必须同时设置 allowJscheckJs。 这在体验中是一个有点烦人的摩擦点,所以 checkJs 现在默认隐含 allowJs

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

React 17 JSX 工厂

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

  • react-jsx
  • react-jsxdev

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

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

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

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

有关更多信息,请查看相应的 PR

对 JSDoc @see 标签的编辑器支持

JSDoc 标签 @see 标签现在在 TypeScript 和 JavaScript 的编辑器中得到了更好的支持。 这允许你在标签后面的点号名称上使用跳转到定义等功能。 例如,在 JSDoc 注释中的 firstC 上跳转到定义在以下示例中就可以正常工作:

ts
// @filename: first.ts
export class C {}

// @filename: main.ts
import * as first from "./first";

/**
 * @see first.C
 */
function related() {}

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

破坏性更改

lib.d.ts 更改

lib.d.ts 可能有一组更改的 API,部分原因可能是 DOM 类型的自动生成方式。 一个具体的更改是 Reflect.enumerate 已被移除,因为它已从 ES2016 中移除。

abstract 成员不能标记为 async

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

any/unknown 在假值位置传播

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

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

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

let x = foo && somethingElse;

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

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

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

通常,适当的修复方法是将 foo && someExpression 切换为 !!foo && someExpression

Promiseresolve 的参数不再是可选的

在编写如下代码时

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

你可能会遇到如下错误:

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

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

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

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

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

TypeScript 4.1 附带了一个快速修复来帮助解决此中断。

条件展开创建可选属性

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

许多用户利用这一点来“有条件地”展开属性。

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,
  };
}

// 我们也可以在这里使用可选链:

function copyOwner(pet?: Animal) {
  return {
    ...pet?.owner,
    otherStuff: 123,
  };
}

这里,如果 pet 被定义,pet.owner 的属性将被展开——否则,没有属性会被展开到返回的对象中。

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

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

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

然而,我们已经看到这种模式被发挥到了极致,在一个对象中进行数百次展开,每次展开可能添加数百或数千个属性。 事实证明,出于各种原因,这最终会非常昂贵,而且通常收益不大。

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

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

这最终会表现得更好,并且通常显示得也更好。

有关更多详细信息,请参阅原始更改。 虽然此行为目前不完全一致,但我们预计未来版本将产生更清晰、更可预测的结果。

不匹配的参数不再关联

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

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