TypeScript 4.0正式发布!现在是开始使用它的最佳时机
如果你还不熟悉 TypeScript,这里简单介绍一下:它是一种在 JavaScript 之上通过添加静态类型语法来构建的语言。它的基本理念是,记下值的类型以及它们的使用位置后,可以使用 TypeScript 对代码进行类型检查,并在运行代码之前(甚至在保存文件之前)告诉你代码错误的相关信息。然后,你可以使用 TypeScript 编译器从代码中剥离类型,并为你提供可在任何地方运行的简洁易读的 JavaScript 代码。除了类型检查之外,TypeScript 还使用静态类型来支持强大的编辑器工具,例如自动完成、代码导航、重构等。实际上,如果你在 Visual Studio Code 或 Visual Studio 这样的编辑器中使用过 JavaScript,那么你已经用上了类型和 TypeScript 带来的体验。可以在我们的网站上了解更多相关信息。
https://www.typescriptlang.org/
TypeScript 4.0 没有引入特别重大的更改。实际上,如果你刚刚开始接触这种语言,那么现在是开始使用它的最佳时机。它的社区已经成熟完善,并在不断发展,拥有可运行的代码和很棒的新资源可供学习。还有一件事情:尽管我们为 4.0 引入了那么多好东西,但你实际上只需要了解 TypeScript 的基础知识就可以开始生产应用了!
npm install -D typescript
你还可以通过以下方式获得编辑器支持:
下载 Visual Studio 2019/2017:
https://marketplace.visualstudio.com/items?itemName=TypeScriptTeam.TypeScript-40
安装 Visual Studio Code 的内部版本,或按照以下说明使用较新版本的 TypeScript。
https://code.visualstudio.com/docs/typescript/typescript-compiling#_using-newer-typescript-versions
TypeScript 是当今许多人的 JavaScript 技术栈的核心部分。在 npm 上,TypeScript 在 7 月首次实现了超过 5000 万的月下载量!尽管我们知道它总有增长和改进的余地,但很明显,大多数使用 TypeScript 编码的开发人员确实很喜欢它。StackOverflow 的最新开发人员调查将 TypeScript 列为第二受欢迎的语言。在最新的 JS 现状调查中,使用 TypeScript 的开发人员中有大约 89% 表示会再次使用它。
值得一提的是我们走到今天所走过的旅程。在之前的两个主要版本中,我们回顾了多年来闪耀的一些亮点。对于 TypeScript 4.0,我们将保持这种传统。
从 3.0 版本向前看,可以看到许多令人眼花缭乱的更改,但是 TypeScript 3.0 本身就产生了很大的冲击。统一元组类型和参数列表是当时的一大亮点,可在函数上启用大量已有的 JavaScript 模式。这个发行版还提供了项目参考,以帮助扩展、组织和共享代码库。3.0 版的一个产生重大影响的小更改是对 any 引入了类型安全的替代方法,称为 unknown。
TypeScript 3.1 扩展了映射类型的功能以处理元组和数组类型,并极大简化了将属性附加到函数的过程,而无需使用 TypeScript 专属的运行时功能(已停用)。
TypeScript 3.2 允许对象在泛型类型上传播,并通过严格类型化 bind、call 和 apply,利用 3.0 的功能更好地建模函数的元编程。TypeScript 3.3 更多关注稳定性,但也改进了联合类型方法,并在 --build 模式下添加了文件增量式构建。
在 3.4 版本中,我们进一步支持函数式模式,更好地支持不可变数据结构,并改进了对高阶泛型函数的推断。这个发行版的一大改进是引入了 --incremental 标志,该方法避免了在每次 TypeScript 运行时完全重建,从而加快了编译和类型检查的速度。
在 TypeScript 3.5 和 3.6 中加强了一些类型系统规则,还带来了更智能的兼容性检查规则。
TypeScript 3.7 是一个非常值得关注的版本,因为它很好地结合了许多新的类型系统特性与 ECMAScript 特性。在类型系统方面,我们加入了递归类型别名引用和对断言样式函数的支持,这两者都是独特的类型系统特性。从 JavaScript 方面来看,该版本带来了可选链和空值合并功能,这是 TypeScript 和 JavaScript 用户最期待的两项功能。
最近,3.8 和 3.9 带来了仅类型的导入 / 导出,以及许多 ECMAScript 特性,例如私有字段、模块中的顶级 await 和新的 export* 语法。这些版本还带来了性能和可伸缩性优化。
我们还没有提到关于语言服务、基础架构、网站和其他核心项目中的那些工作,这些工作对于 TypeScript 的体验非常关键。核心团队的项目之外,我们在生态系统中还有非常出色的贡献者社区,他们推动了体验的不断改进,并通过 DefinitelyTyped 甚至 TypeScript 本身提供了帮助。在 2012 年刚开始时,DefinitelyTyped 仅有 80 个拉取请求。在 2019 年,它有超过 8300 个拉取请求,非常震撼人心。这些贡献是 TypeScript 体验的基础,这样一个繁忙而热情的社区在不断改善我们的生态系统,并推动我们不断改进,我们对此表示感谢。
function concat(arr1, arr2) {
return [...arr1, ...arr2];
}
function tail(arg) {
const [_, ...result] = arg;
return result
}
function concat<>(arr1: [], arr2: []): [A];
function concat<A>(arr1: [A], arr2: []): [A];
function concat<A, B>(arr1: [A, B], arr2: []): [A, B];
function concat<A, B, C>(arr1: [A, B, C], arr2: []): [A, B, C];
function concat<A, B, C, D>(arr1: [A, B, C, D], arr2: []): [A, B, C, D];
function concat<A, B, C, D, E>(arr1: [A, B, C, D, E], arr2: []): [A, B, C, D, E];
function concat<A, B, C, D, E, F>(arr1: [A, B, C, D, E, F], arr2: []): [A, B, C, D, E, F];)
function concat<A2>(arr1: [], arr2: [A2]): [A2];
function concat<A1, A2>(arr1: [A1], arr2: [A2]): [A1, A2];
function concat<A1, B1, A2>(arr1: [A1, B1], arr2: [A2]): [A1, B1, A2];
function concat<A1, B1, C1, A2>(arr1: [A1, B1, C1], arr2: [A2]): [A1, B1, C1, A2];
function concat<A1, B1, C1, D1, A2>(arr1: [A1, B1, C1, D1], arr2: [A2]): [A1, B1, C1, D1, A2];
function concat<A1, B1, C1, D1, E1, A2>(arr1: [A1, B1, C1, D1, E1], arr2: [A2]): [A1, B1, C1, D1, E1, A2];
function concat<A1, B1, C1, D1, E1, F1, A2>(arr1: [A1, B1, C1, D1, E1, F1], arr2: [A2]): [A1, B1, C1, D1, E1, F1, A2];
function concat<T, U>(arr1: T[], arr2, U[]): Array<T | U>;
但在使用元组时,这个签名不会包含输入长度或元素顺序的任何信息。TypeScript 4.0 带来了两个基础更改,并在推断方面进行了改进,从而可以类型化这些内容。
第一个更改是元组类型语法中的 spread 现在可以泛型。这意味着即使我们不知道要操作的实际类型,也可以表示对元组和数组的高阶操作。在这些元组类型中实例化泛型 spread(或用真实类型替换)时,它们可以产生其他数组和元组类型集。
function tail<T extends any[]>(arr: readonly [any, ...T]) {
const [_ignored, ...rest] = arr;
return rest;
}
const myTuple = [1, 2, 3, 4] as const;
const myArray = ["hello", "world"];
// type [2, 3, 4]
const r1 = tail(myTuple);
// type [2, 3, ...string[]]
const r2 = tail([...myTuple, ...myArray] as const);
type Strings = [string, string];
type Numbers = [number, number];
// [string, string, number, number]
type StrStrNumNum = [...Strings, ...Numbers];
A rest element must be last in a tuple type.
但是现在这种限制取消了。
type Strings = [string, string];
type Numbers = number[]
// [string, string, ...Array]
type Unbounded = [...Strings, ...Numbers, boolean];
type Arr = readonly any[];
function concat<T extends Arr, U extends Arr>(arr1: T, arr2: U): [...T, ...U] {
return [...arr1, ...arr2];
}
尽管一个签名仍然有些冗长,但它毕竟只有一个,只需写一次,并且在所有数组和元组上都具有可预测的行为。
function partialCall(f, ...headArgs) {
return (...tailArgs) => f(...headArgs, ...tailArgs)
}
type Arr = readonly unknown[];
function partialCall<T extends Arr, U extends Arr, R>(f: (...args: [...T, ...U]) => R, ...headArgs: T) {
return (...b: U) => f(...headArgs, ...b)
}
const foo = (x: string, y: number, z: boolean) => {}
// This doesn't work because we're feeding in the wrong type for 'x'.
const f1 = partialCall(foo, 100);
// ~~~
// error! Argument of type 'number' is not assignable to parameter of type 'string'.
// This doesn't work because we're passing in too many arguments.
const f2 = partialCall(foo, "hello", 100, true, "oops")
// ~~~~~~
// error! Expected 4 arguments, but got 5.
// This works! It has the type '(y: number, z: boolean) => void'
const f3 = partialCall(foo, "hello");
// What can we do with f3 now?
f3(123, true); // works!
f3();
// error! Expected 2 arguments, but got 0.
f3(123, "hello");
// ~~~~~~~
// error! Argument of type '"hello"' is not assignable to parameter of type 'boolean'.
可变元组类型创造了许多新模式,尤其是在函数组合方面。我们希望利用它来改善对 JavaScript 内置的 bind 方法的类型检查。此外还有其他一些推断改进和模式,想了解更多信息,可以查看可变元组的拉取请求。
https://github.com/microsoft/TypeScript/pull/39094
改善元组类型和参数列表的体验很重要,因为它使我们能够围绕常见的 JavaScript 习惯用法进行强类型验证——实际上只是对参数列表进行切片和切块,并将它们传递给其他函数。对 rest 参数使用元组类型是其中的关键。
function foo(...args: [string, number]): void {
// ...
}
function foo(arg0: string, arg1: number): void {
// ...
}
foo("hello", 42); // works
foo("hello", 42, true); // error
foo("hello"); // error
type Range = [start: number, end: number];
type Foo = [first: number, second?: string, ...rest: any[]];
type Bar = [first: string, number];
// ~~~~~~
// error! Tuple members must all have names or all not have names.
function foo(x: [first: string, second: number]) {
// ...
// note: we didn't need to name these 'first' and 'second'
let [a, b] = x;
// ...
}
总的来说,当利用围绕元组和参数列表的模式,并以类型安全的方式实现重载时,带标记的元组非常方便好用。实际上,TypeScript 的编辑器支持会在可能的情况下将它们显示为重载。
了解更多信息,请查看带标记的元组元素的拉取请求。
https://github.com/microsoft/TypeScript/pull/38234
class Square {
// Previously: implicit any!
// Now: inferred to `number`!
area;
sideLength;
constructor(sideLength: number) {
this.sideLength = sideLength;
this.area = sideLength ** 2;
}
}
class Square {
sideLength;
constructor(sideLength: number) {
if (Math.random()) {
this.sideLength = sideLength;
}
}
get area() {
return this.sideLength ** 2;
// ~~~~~~~~~~~~~~~
// error! Object is possibly 'undefined'.
}
}
class Square {
// definite assignment assertion
// v
sideLength!: number;
// ^^^^^^^^
// type annotation
constructor(sideLength: number) {
this.initialize(sideLength)
}
initialize(sideLength: number) {
this.sideLength = sideLength;
}
get area() {
return this.sideLength ** 2;
}
}
更多信息请见拉取请求。
https://github.com/microsoft/TypeScript/pull/379200
// Addition
// a = a + b
a += b;
// Subtraction
// a = a - b
a -= b;
// Multiplication
// a = a * b
a *= b;
// Division
// a = a / b
a /= b;
// Exponentiation
// a = a ** b
a **= b;
// Left Bit Shift
// a = a << b
a <<= b;
JavaScript 中有很多运算符都有对应的赋值运算符!但是有三个值得注意的例外:逻辑和(&&),逻辑或(||)和空值合并(??)。
所以 TypeScript 4.0 支持了一个新的 ECMAScript 特性,添加了三个新的赋值运算符:&&=,||= 和 ??= 。
a = a && b;
a = a || b;
a = a ?? b;
// could be 'a ||= b'
if (!a) {
a = b;
}
let values: string[];
// Before
(values ?? (values = [])).push("hello");
// After
(values ??= []).push("hello");
obj.prop ||= foo();
// roughly equivalent to either of the following
obj.prop || (obj.prop = foo());
if (!obj.prop) {
obj.prop = foo();
}
const obj = {
get prop() {
console.log("getter has run");
// Replace me!
return Math.random() < 0.5;
},
set prop(_val: boolean) {
console.log("setter has run");
}
};
function foo() {
console.log("right side evaluated");
return true;
}
console.log("This one always runs the setter");
obj.prop = obj.prop || foo();
console.log("This one *sometimes* runs the setter");
obj.prop ||= foo();
有关更多细节可以查看拉取请求。
https://github.com/microsoft/TypeScript/pull/37727
你也可以查看 TC39 的提案存储库。
https://github.com/tc39/proposal-logical-assignment/
try {
// ...
}
catch (x) {
// x has type 'any' - have fun!
console.log(x.message);
console.log(x.toUpperCase());
x++;
x.yadda.yadda.yadda();
}
try {
// ...
}
catch (e: unknown) {
// error!
// Property 'toUpperCase' does not exist on type 'unknown'.
console.log(e.toUpperCase());
if (typeof e === "string") {
// works!
// We've narrowed 'e' down to the type 'string'.
console.log(e.toUpperCase());
}
}
尽管默认情况下 catch 变量的类型不会更改,但我们将来可能会考虑使用新的 --strict 模式标志,以便用户选择启用此行为。同时,应该可以编写一个 lint 规则来强制 catch 变量具有如下显式注解之一:: any 或: unknown。
有关更多信息,可以查看拉取请求。
https://github.com/microsoft/TypeScript/pull/39015
使用 JSX 时,fragment 是 JSX 元素的一种,允许我们返回多个子元素。当我们第一次在 TypeScript 中实现 fragment 时,我们对其他库如何利用它们并不了解。如今,大多数鼓励使用 JSX 和支持 fragment 的库都具有类似的 API 设计。
在 TypeScript 4.0 中,用户可以通过新的 jsxFragmentFactory 选项来自定义 fragment 工厂。
{
"compilerOptions": {
"target": "esnext",
"module": "commonjs",
"jsx": "react",
"jsxFactory": "h",
"jsxFragmentFactory": "Fragment"
}
}
// Note: these pragma comments need to be written
// with a JSDoc-style multiline syntax to take effect.
/** @jsx h */
/** @jsxFrag Fragment */
import { h, Fragment } from "preact";
let stuff = <>
<div>Hellodiv>
>;
// Note: these pragma comments need to be written
// with a JSDoc-style multiline syntax to take effect.
/** @jsx h */
/** @jsxFrag Fragment */
import { h, Fragment } from "preact";
let stuff = h(Fragment, null,
h("div", null, "Hello"));
查看拉取请求以获取更多信息。
https://github.com/microsoft/TypeScript/pull/38720
以前,使用 --noEmitOnError 标志时,当先前的编译在 --incremental 下出现错误,编译速度将非常缓慢。这是因为基于 --noEmitOnError 标志,上次编译的任何信息都不会缓存在.tsbuildinfo 文件中。
TypeScript 4.0 对此进行了更改,从而在这些情况下极大地提高了速度,进而改进了 --build 模式的场景(这意味着同时有 --incremental 和 --noEmitOnError)。
有关详细信息,请查看拉取请求。
https://github.com/microsoft/TypeScript/pull/38853
TypeScript 4.0 允许我们在利用 --incremental 编译时使用 --noEmit 标志。以前不允许这样做,因为 --incremental 需要发出.tsbuildinfo 文件。
有关详细信息,请查看拉取请求。
https://github.com/microsoft/TypeScript/pull/39122
TypeScript 编译器不仅可以为大多数主流编辑器提供较好的 TS 编辑体验,还可以改进 Visual Studio 系列编辑器的 JavaScript 开发体验。
根据你使用的编辑器,在编辑器中使用新的 TypeScript/JavaScript 功能时会有区别:
Visual Studio Code 支持选择不同版本的 TypeScript。另外,还有 JavaScript/TypeScript Nightly Extension 来紧跟最新版本(通常非常稳定)。
Visual Studio 2017/2019 有上面的 SDK 安装程序和 MSBuild 安装。
更多信息见 TS 编辑器支持列表。
https://github.com/Microsoft/TypeScript/wiki/TypeScript-Editor-Support
可选链是一项新功能,受到了广泛的欢迎。TypeScript 4.0 在转换常见模式时可以利用可选链和空值合并的优势!
我们认为这种重构应该能捕获大多数用例的意图,尤其是当 TypeScript 对你的类型有更精确的了解时。
有关详细信息,请查看拉取请求。
https://github.com/microsoft/TypeScript/pull/39135
现在,TypeScript 的编辑支持可以识别声明中是否带有/** @deprecated*/
JSDoc 注释。该信息显示在自动完成列表中,并作为编辑器可以特别处理的建议诊断。在像 VSCode 这样的编辑器中,deprecated 的值通常显示为删除线样式。
有关详细信息,查看拉取请求。
https://github.com/microsoft/TypeScript/pull/38523
很多用户抱怨启动时间缓慢,尤其是在大型项目中。具体来说,罪魁祸首通常是一个称为项目加载的过程,该过程与我们编译器的程序构建步骤大致相同。这一过程从一组初始文件开始,解析它们、解析它们的依赖、再解析那些依赖,解析那些依赖的依赖,等等,最后需要花费很长时间。项目越大,启动延迟可能会越长。
所以我们一直在努力为开发人员提供一种新的模式,在获得完整的语言服务体验之前提供部分体验。这里的核心思想是,编辑者可以运行仅具有单个文件视图的轻量级部分服务器。
这种新模式可以将 TypeScript 在代码库上开始交互之前的准备时间从 20 秒到 1 分钟缩短到只有几秒钟。比如说,在较大的代码库上重启编辑器时,TS 3.9 版没法立即提供自动完成和快速信息;另一方面,TS 4.0 可以立即提供完整的编辑体验,同时在后台加载整个项目。
当前,唯一支持此模式的编辑器是 Visual Studio Code,但 UX 和功能仍有改进的余地。我们列出了准备加入的改进,希望获得更多反馈。
https://github.com/microsoft/TypeScript/issues/39035
有关更多信息,你可以查看原始提案,拉取请求,以及后续的 meta 问题。
https://github.com/microsoft/TypeScript/issues/37713
自动导入是一个了不起的功能。但是,自动导入在用 TypeScript 编写的包上不起作用——也就是说,我们得在项目的其他位置至少写了一个显式导入。
为什么自动导入适用于 @types 软件包,而不适用于使用自己类型的包呢?其实自动导入是通过检查项目中已经包含的软件包来实现的。TypeScript 有一个怪癖,可以自动包括 node_modules/@types 中的所有包,而忽略其他包;但爬取所有 node_modules 包的开销可能会很昂贵。
当你尝试自动导入刚刚安装但尚未使用的内容时,这些都会导致糟糕的体验。
TypeScript 4.0 现在可以包含你在 package.json 的 dependencies(和 peerDependencies)字段中列出的包。这些包中的信息仅用于改进自动导入,不会更改类型检查等其他内容。这样就避免了遍历 node_modules 目录的成本,使我们可以为所有带类型的依赖项提供自动导入。
当你的 package.json 列出了超过十项尚未导入的类型化依赖项时,这个功能会自动禁用,以避免缓慢的项目加载过程。要强制开启它或完全禁用它,你可以配置编辑器。在 Visual Studio Code 中是"Include Package JSON Auto Imports"设置(或 typescript.preferences.includePackageJsonAutoImports)。
有关详细信息,可以查看提案问题以及拉取请求。
https://github.com/microsoft/TypeScript/issues/37812
TypeScript 网站最近被彻底重写了!
详细信息可以参考之前的文章:
我们的 lib.d.ts 声明已更改,具体来说是 DOM 的类型已更改。主要是删除了 document.origin,它仅在 IE 的旧版本中有效,而 Safari MDN 建议改用 self.origin。
class Base {
get foo() {
return 100;
}
set foo() {
// ...
}
}
class Derived extends Base {
foo = 10;
// ~~~
// error!
// 'foo' is defined as an accessor in class 'Base',
// but is overridden here in 'Derived' as an instance property.
}
class Base {
prop = 10;
}
class Derived extends Base {
get prop() {
// ~~~~
// error!
// 'prop' is defined as a property in class 'Base', but is overridden here in 'Derived' as an accessor.
return 100;
}
}
有关详细信息,查看拉取请求。
https://github.com/microsoft/TypeScript/pull/37894
interface Thing {
prop: string;
}
function f(x: Thing) {
delete x.prop;
// ~~~~~~
// error! The operand of a 'delete' operator must be optional.
}
关于更多信息,查看拉取请求。
https://github.com/microsoft/TypeScript/pull/37921
如今,TypeScript 提供了一组用于生成 AST 节点的“工厂”函数。但是,TypeScript 4.0 提供了新的 node 工厂 API。因此 TypeScript 4.0 决定弃用使用这些旧函数,推荐改用新函数。
有关更多信息,请查看拉取请求。
https://github.com/microsoft/TypeScript/pull/35282
TypeScript 4.1 的迭代计划已经上线了,你可以大致了解一下。
https://github.com/microsoft/TypeScript/issues/40124
同时,你可以在工作区或编辑器中使用 nightly 构建来预览 4.1 中添加的新特性。无论你是在使用 TypeScript 4.0 还是下一版本,我们都希望听到你的反馈!可以通过 Twitter 联系我们,或在 GitHub 上发起问题。
我们再一次为社区所做的一切工作和奉献精神深表感谢。我们希望让 TypeScript 和 JavaScript 的编码体验成为你应得的纯粹乐趣。为此,我们需要改善语言和编辑体验、提升性能、迭代我们的用户体验、降低入门和学习的门槛等等。
非常感谢大家,请享用 4.0 版本吧,编程愉快!
https://devblogs.microsoft.com/typescript/announcing-typescript-4-0/