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

TypeScript 3.4

使用 --incremental 标志加快后续构建

TypeScript 3.4 引入了一个名为 incremental 的新标志,它告诉 TypeScript 保存上次编译的项目图信息。 下次使用 incremental 调用 TypeScript 时,它将使用该信息以最低成本的方式检测类型检查和输出更改到你的项目。

jsonc
// tsconfig.json
{
  "compilerOptions": {
    "incremental": true,
    "outDir": "./lib"
  },
  "include": ["./src"]
}

默认情况下,使用这些设置,当我们运行 tsc 时,TypeScript 将在输出目录(./lib)中查找名为 .tsbuildinfo 的文件。 如果 ./lib/.tsbuildinfo 不存在,它将被生成。 但如果它存在,tsc 将尝试使用该文件来增量类型检查和更新我们的输出文件。

这些 .tsbuildinfo 文件可以安全删除,并且对我们的代码在运行时没有任何影响——它们纯粹用于加快编译速度。 我们也可以任意命名它们,并使用 tsBuildInfoFile 选项将它们放在任何我们想要的位置。

jsonc
// front-end.tsconfig.json
{
  "compilerOptions": {
    "incremental": true,
    "tsBuildInfoFile": "./buildcache/front-end",
    "outDir": "./lib"
  },
  "include": ["./src"]
}

复合项目

复合项目(将 composite 设置为 truetsconfig.json)的部分意图是不同项目之间的引用可以增量构建。 因此,复合项目将始终生成 .tsbuildinfo 文件。

outFile

当使用 outFile 时,构建信息文件的名称将基于输出文件的名称。 例如,如果我们的输出 JavaScript 文件是 ./output/foo.js,那么在 incremental 标志下,TypeScript 将生成文件 ./output/foo.tsbuildinfo。 如上所述,这可以通过 tsBuildInfoFile 选项控制。

泛型函数的高阶类型推断

TypeScript 3.4 现在可以在从其他泛型函数进行推断为推断产生自由类型变量时生成泛型函数类型。 这意味着许多函数组合模式现在在 3.4 中工作得更好。

更具体地说,让我们构建一些动机并考虑以下 compose 函数:

ts
function compose<A, B, C>(f: (arg: A) => B, g: (arg: B) => C): (arg: A) => C {
  return (x) => g(f(x));
}

compose 接受另外两个函数:

  • f 接受某个参数(类型为 A)并返回类型为 B 的值
  • g 接受类型为 B 的参数(f 返回的类型),并返回类型为 C 的值

然后 compose 返回一个函数,该函数将其参数通过 f 然后 g 传递。

调用此函数时,TypeScript 将尝试通过称为类型参数推断的过程找出 ABC 的类型。 这个推断过程通常工作得很好:

ts
interface Person {
  name: string;
  age: number;
}

function getDisplayName(p: Person) {
  return p.name.toLowerCase();
}

function getLength(s: string) {
  return s.length;
}

// 具有类型 '(p: Person) => number'
const getDisplayNameLength = compose(getDisplayName, getLength);

// 有效并返回类型 'number'
getDisplayNameLength({ name: "Person McPersonface", age: 42 });

这里的推断过程相当直接,因为 getDisplayNamegetLength 使用了可以轻松引用的类型。 然而,在 TypeScript 3.3 及更早版本中,像 compose 这样的泛型函数在传递其他泛型函数时效果并不好。

ts
interface Box<T> {
  value: T;
}

function makeArray<T>(x: T): T[] {
  return [x];
}

function makeBox<U>(value: U): Box<U> {
  return { value };
}

// 具有类型 '(arg: {}) => Box<{}[]>'
const makeBoxedArray = compose(makeArray, makeBox);

makeBoxedArray("hello!").value[0].toUpperCase();
//                                ~~~~~~~~~~~
// 错误:属性 'toUpperCase' 在类型 '{}' 上不存在。

在旧版本中,TypeScript 在从其他类型变量(如 TU)推断时会推断出空对象类型({})。

在 TypeScript 3.4 的类型参数推断过程中,对于返回函数类型的泛型函数的调用,TypeScript 适当地将泛型函数参数的类型参数传播到结果函数类型中。

换句话说,TypeScript 3.4 没有产生类型

ts
(arg: {}) => Box<{}[]>

而是产生类型

ts
<T>(arg: T) => Box<T[]>

注意 T 已经从 makeArray 传播到结果类型的类型参数列表中。 这意味着 compose 参数的泛型性被保留了,我们的 makeBoxedArray 示例将正常工作!

ts
interface Box<T> {
  value: T;
}

function makeArray<T>(x: T): T[] {
  return [x];
}

function makeBox<U>(value: U): Box<U> {
  return { value };
}

// 具有类型 '<T>(arg: T) => Box<T[]>'
const makeBoxedArray = compose(makeArray, makeBox);

// 毫无问题地工作!
makeBoxedArray("hello!").value[0].toUpperCase();

有关更多详细信息,你可以在原始更改中阅读更多

ReadonlyArrayreadonly 元组的改进

TypeScript 3.4 使使用只读类数组类型变得更容易一些。

ReadonlyArray 的新语法

ReadonlyArray 类型描述了只能从中读取的 Array。 任何对 ReadonlyArray 的引用变量都不能添加、删除或替换数组的任何元素。

ts
function foo(arr: ReadonlyArray<string>) {
  arr.slice(); // 可以
  arr.push("hello!"); // 错误!
}

虽然在不打算修改时使用 ReadonlyArray 而不是 Array 是一种好的做法,但由于数组有更简洁的语法,这通常很麻烦。 具体来说,number[]Array<number> 的简写,就像 Date[]Array<Date> 的简写一样。

TypeScript 3.4 为 ReadonlyArray 引入了一种新语法,使用一个新的 readonly 修饰符用于数组类型。

ts
function foo(arr: readonly string[]) {
  arr.slice(); // 可以
  arr.push("hello!"); // 错误!
}

readonly 元组

TypeScript 3.4 还引入了对 readonly 元组的新支持。 我们可以用 readonly 关键字为任何元组类型添加前缀,使其成为 readonly 元组,就像我们现在可以使用数组简写语法一样。 正如你所期望的,与可以写入其插槽的普通元组不同,readonly 元组只允许从这些位置读取。

ts
function foo(pair: readonly [string, string]) {
  console.log(pair[0]); // 可以
  pair[1] = "hello!"; // 错误
}

就像普通元组是扩展自 Array 的类型——一个元素类型为 T1T2、... Tn 的元组扩展自 Array< T1 | T2 | ... Tn >——readonly 元组是扩展自 ReadonlyArray 的类型。因此,一个元素为 T1T2、... Tnreadonly 元组扩展自 ReadonlyArray< T1 | T2 | ... Tn

readonly 映射类型修饰符和 readonly 数组

在 TypeScript 的早期版本中,我们泛化了映射类型以在类数组类型上以不同方式操作。 这意味着像 Boxify 这样的映射类型可以在数组和元组上同样工作。

ts
interface Box<T> {
  value: T;
}

type Boxify<T> = {
  [K in keyof T]: Box<T[K]>;
};

// { a: Box<string>, b: Box<number> }
type A = Boxify<{ a: string; b: number }>;

// Array<Box<number>>
type B = Boxify<number[]>;

// [Box<string>, Box<number>]
type C = Boxify<[string, boolean]>;

不幸的是,像 Readonly 实用类型这样的映射类型在数组和元组类型上实际上是无操作的。

ts
// lib.d.ts
type Readonly<T> = {
  readonly [K in keyof T]: T[K];
};

// 代码在 *TypeScript 3.4 之前* 的行为

// { readonly a: string, readonly b: number }
type A = Readonly<{ a: string; b: number }>;

// number[]
type B = Readonly<number[]>;

// [string, boolean]
type C = Readonly<[string, boolean]>;

在 TypeScript 3.4 中,映射类型中的 readonly 修饰符将自动将类数组类型转换为其对应的 readonly 对应物。

ts
// 代码现在在 *TypeScript 3.4 中* 的行为

// { readonly a: string, readonly b: number }
type A = Readonly<{ a: string; b: number }>;

// readonly number[]
type B = Readonly<number[]>;

// readonly [string, boolean]
type C = Readonly<[string, boolean]>;

类似地,你可以编写一个实用类型,如 Writable 映射类型,它移除 readonly 属性,并将 readonly 数组容器转换回其可变的对应物。

ts
type Writable<T> = {
  -readonly [K in keyof T]: T[K];
};

// { a: string, b: number }
type A = Writable<{
  readonly a: string;
  readonly b: number;
}>;

// number[]
type B = Writable<readonly number[]>;

// [string, boolean]
type C = Writable<readonly [string, boolean]>;

注意事项

尽管看起来如此,readonly 类型修饰符只能用于数组类型和元组类型的语法。 它不是通用的类型运算符。

ts
let err1: readonly Set<number>; // 错误!
let err2: readonly Array<boolean>; // 错误!

let okay: readonly boolean[]; // 正常工作

你可以在拉取请求中查看更多详细信息

const 断言

TypeScript 3.4 为字面量值引入了一种新的构造,称为 const 断言。 它的语法是使用 const 代替类型名称的类型断言(例如 123 as const)。 当我们使用 const 断言构造新的字面量表达式时,我们可以向语言发出信号:

  • 该表达式中的任何字面量类型都不应被拓宽(例如,不会从 "hello" 变成 string
  • 对象字面量获得 readonly 属性
  • 数组字面量变成 readonly 元组
ts
// 类型 '"hello"'
let x = "hello" as const;

// 类型 'readonly [10, 20]'
let y = [10, 20] as const;

// 类型 '{ readonly text: "hello" }'
let z = { text: "hello" } as const;

.tsx 文件之外,也可以使用尖括号断言语法。

ts
// 类型 '"hello"'
let x = <const>"hello";

// 类型 'readonly [10, 20]'
let y = <const>[10, 20];

// 类型 '{ readonly text: "hello" }'
let z = <const>{ text: "hello" };

此特性意味着,通常仅用于向编译器提示不可变性的类型通常可以被省略。

ts
// 没有引用或声明的类型也能工作。
// 我们只需要一个 const 断言。
function getShapes() {
  let result = [
    { kind: "circle", radius: 100 },
    { kind: "square", sideLength: 50 },
  ] as const;

  return result;
}

for (const shape of getShapes()) {
  // 完美收窄!
  if (shape.kind === "circle") {
    console.log("Circle radius", shape.radius);
  } else {
    console.log("Square side length", shape.sideLength);
  }
}

注意上面不需要任何类型注解。 const 断言允许 TypeScript 采用表达式的最具体类型。

如果你选择不使用 TypeScript 的 enum 构造,这甚至可以在纯 JavaScript 代码中启用类似 enum 的模式。

ts
export const Colors = {
  red: "RED",
  blue: "BLUE",
  green: "GREEN",
} as const;

// 或使用 'export default'

export default {
  red: "RED",
  blue: "BLUE",
  green: "GREEN",
} as const;

注意事项

需要注意的一点是,const 断言只能直接应用于简单的字面量表达式。

ts
// 错误!'const' 断言只能应用于
// 字符串、数字、布尔值、数组或对象字面量。
let a = (Math.random() < 0.5 ? 0 : 1) as const;
let b = (60 * 60 * 1000) as const;

// 有效!
let c = Math.random() < 0.5 ? (0 as const) : (1 as const);
let d = 3_600_000 as const;

另一件要记住的事情是,const 上下文不会立即将表达式转换为完全不可变。

ts
let arr = [1, 2, 3, 4];

let foo = {
  name: "foo",
  contents: arr,
} as const;

foo.name = "bar"; // 错误!
foo.contents = []; // 错误!

foo.contents.push(5); // ...有效!

有关更多详细信息,你可以查看相应的拉取请求

globalThis 的类型检查

TypeScript 3.4 引入了对 ECMAScript 新的 globalThis 的类型检查支持——一个全局变量,嗯,它引用全局作用域。 与上述解决方案不同,globalThis 提供了一种访问全局作用域的标准方式,可以在不同环境中使用。

ts
// 在一个全局文件中:

var abc = 100;

// 引用上面的 'abc'。
globalThis.abc = 200;

注意使用 letconst 声明的全局变量不会出现在 globalThis 上。

ts
let answer = 42;

// 错误!属性 'answer' 在类型 'typeof globalThis' 上不存在。
globalThis.answer = 333333;

同样重要的是要注意,TypeScript 在编译到旧版本的 ECMAScript 时不会转换对 globalThis 的引用。 因此,除非你的目标是已经支持 globalThis 的常青浏览器,否则你可能希望使用适当的 polyfill 代替。

有关实现的更多详细信息,请参阅该功能的拉取请求