TypeScript 4.2 Beta版本发布:带来诸多更新,营造更好的开发体验
要安装这个 beta 版本,一种方法是使用 NuGet 获取:
https://www.nuget.org/packages/Microsoft.TypeScript.MSBuild
npm install typescript@beta
可以通过以下方式获取编辑器支持:
下载 Visual Studio 2019/2017;
遵循 Visual Studio Code 和 Sublime Text 的指南。
下面就来看看 TypeScript 4.2 带来了哪些新内容。
// A tuple that stores a pair of numbers
let a: [number, number] = [1, 2];
// A tuple that stores a string, a number, and a boolean
let b: [string, number, boolean] = ["hello", 42, true];
// A tuple that has either one or two strings.
let c: [string, string?] = ["hello"];
c = ["hello", "world"];
// A labeled tuple that has either one or two strings.
let d: [first: string, second?: string] = ["hello"];
d = ["hello", "world"];
// A tuple with a *rest element* - holds at least 2 strings at the front,
// and any number of booleans at the back.
let e: [string, string, ...boolean[]];
e = ["hello", "world"];
e = ["hello", "world", false];
e = ["hello", "world", true, false, true];
let foo: [...string[], number];
foo = [123];
foo = ["hello", 123];
foo = ["hello!", "hello!", "hello!", 123];
let bar: [boolean, ...string[], boolean];
bar = [true, false];
bar = [true, "some text", false];
bar = [true, "some", "separated", "text", false];
interface Clown { /*...*/ }
interface Joker { /*...*/ }
let StealersWheel: [...Clown[], "me", ...Joker[]];
// ~~~~~~~~~~ Error!
// A rest element cannot follow another rest element.
let StringssAndMaybeBoolean: [...string[], boolean?];
// ~~~~~~~~ Error!
// An optional element cannot follow a rest element.
declare function doStuff(...args: [...names: string[], shouldCapitalize: boolean]): void;
doStuff(/*shouldCapitalize:*/ false)
doStuff("fee", "fi", "fo", "fum", /*shouldCapitalize:*/ true);
虽然 JavaScript 没有任何语法来建模前置 rest 参数,但我们可以声明...args rest 参数和一个使用前置 rest 元素的元组类型,来将 doStuff 声明为采用前置参数的函数。这样就可以建模许多现有的 JavaScript 代码了!
详细信息请参见原始拉取请求:
https://github.com/microsoft/TypeScript/pull/41544
export type BasicPrimitive = number | string | boolean;
export function doStuff(value: BasicPrimitive) {
let x = value;
return x;
}
如果在 Visual Studio、Visual Studio Code 或 TypeScript Playground 等编辑器中将鼠标悬停在 x 上,我们将看到一个快速信息面板,其中显示了 BasicPrimitive 类型。同样,如果我们获得此文件的声明文件输出(.d.ts 输出),TypeScript 会告诉你 doStuff 返回 BasicPrimitive。
export type BasicPrimitive = number | string | boolean;
export function doStuff(value: BasicPrimitive) {
if (Math.random() < 0.5) {
return undefined;
}
return value;
}
以 TypeScript Playground 为例。虽然我们可能希望 TypeScript 将 doStuff 的返回类型显示为 BasicPrimitive | undefined,但它实际显示的是 string | number | boolean | undefined!什么鬼?
其实这和 TypeScript 在内部表示类型的方法有关。从一个或多个联合类型创建一个联合类型时,它总会将这些类型规范化为一个新的展平联合类型——但这会丢失信息。类型检查器是没办法知道 string | number | boolean 来自哪里的。
在 TypeScript 4.2 中,我们的内部结构更加智能了。在规范化类型之前,我们会保留其原始结构的某些部分来跟踪类型的构造方式。我们还将跟踪并区分类型别名和其他别名实例!
现在系统能够根据你在代码中的使用方式来打印出这些类型,这意味着作为 TypeScript 用户,你可以避免显示一些烦人的巨大类型,而这往往会转化为更好的.d.ts 文件输出、错误消息和快速信息及签名帮助中的编辑器内类型显示。
有关更多信息,请查看拉取请求:
https://github.com/microsoft/TypeScript/pull/42149
https://github.com/microsoft/TypeScript/pull/42284
type GreetingStart = "hello" | "hi" | "sup";
declare function greet(str: `${GreetingStart} ${string}`): void;
// Works.
greet("hello world!");
// Works.
greet("hi everybody!");
// Error!
// Doesn't work with any of the patterns:
// `hello ${string}` | `hi ${string}` | `sup ${string}`
greet("hallo yes hi sup");
function doStuff(str: string) {
// Error!
// Type 'string' is not assignable to type '`hello ${string}`'.
let x: `hello ${string}` = `hello ${str}`
}
这是因为带有替换插槽 ${likeThis}的模板字符串表达式总是只有 string 类型。于是它们可能与我们新的模板字符串类型不兼容。
const n: number = 123;
// Has the type `${number}px`
const s1 = `${n}px`;
// Works!
const s2: `${number}px` = s1;
// Error!
const s3: `${number}pt` = s1;
// Has the type 'string' because of widening.
let v1 = s1;
详情查看拉取请求:
https://github.com/microsoft/TypeScript/pull/41891
"foo" in 42
// ~~
// error! The right-hand side of an 'in' expression must not be a primitive.
这个检查在大多数情况下是相当保守的,因此如果你收到与此相关的错误,表明问题可能出在代码中。非常感谢我们的外部贡献者 Jonas Hübotter 的拉取请求:
https://github.com/microsoft/TypeScript/pull/41928
interface SomeType {
/** This is an index signature. */
[propName: string]: any;
}
function doStuff(value: SomeType) {
let x = value["someProperty"];
}
interface Options {
/** File patterns to be excluded. */
exclude?: string[];
/**
* It handles any extra properties that we haven't declared as type 'any'.
*/
[x: string]: any;
}
function processOptions(opts: Options) {
// Notice we're *intentionally* accessing `excludes`, not `exclude`
if (opts.excludes) {
console.error("The option `excludes` is not valid. Did you mean `exclude`?");
}
}
为了简化这类场景的操作,前不久 TypeScript 在类型带有一个字符串索引签名时加入了“点”属性访问语法(例如 person.name)。这样将原有 JavaScript 代码过渡到 TypeScript 也简单了一些。
function processOptions(opts: Options) {
// ...
// Notice we're *accidentally* accessing `excludes` this time.
// Oops! Totally valid.
for (const excludePattern of opts.excludes) {
// ...
}
}
在某些情况下,用户希望显式选择加入索引签名——当点属性访问与特定的属性声明不对应时,他们希望收到错误消息。
所以 TypeScript 引入了一个名为 --noPropertyAccessFromIndexSignature 的新标志。在这种模式下,你将选择使用 TypeScript 的旧款行为,跳出一个错误。这个新设置不受 strict 标志族的限制,因为我们相信用户会发现它在某些代码库上更好用。
详细信息请查看拉取请求,感谢 Wenlu Wang 的贡献。
https://github.com/microsoft/TypeScript/pull/40171/
abstract class Shape {
abstract getArea(): number;
}
// Error! Can't instantiate an abstract class.
new Shape();
class Square extends Shape {
#sideLength: number;
constructor(sideLength: number) {
this.#sideLength = sideLength;
}
getArea() {
return this.#sideLength ** 2;
}
}
// Works fine.
new Square(42);
interface HasArea {
getArea(): number;
}
// Error! Cannot assign an abstract constructor type to a non-abstract constructor type.
let Ctor: new () => HasArea = Shape;
functon makeSubclassWithArea(Ctor: new () => HasArea) {
return class extends Ctor {
getArea() {
// ...
}
}
}
let MyShape = makeSubclassWithArea(Shape);
// Error!
// Type 'typeof Shape' does not satisfy the constraint 'new (...args: any) => any'.
// Cannot assign an abstract constructor type to a non-abstract constructor type.
type MyInstance = InstanceType<typeof Shape>;
interface HasArea {
getArea(): number;
}
// Works!
let Ctor: abstract new () => HasArea = Shape;
// ^^^^^^^^
将 abstract 修饰符添加到一个构造签名,表示你可以传递 abstract 构造函数。这并不会阻止你传递其他“具体”的类 / 构造函数——它实际上只是表明没有意图直接运行构造函数,因此可以安全地传递任何一种类类型。
abstract class SuperClass {
abstract someMethod(): void;
badda() {}
}
type AbstractConstructor = abstract new (...args: any[]) => T
function withStyles>(Ctor: T) {
abstract class StyledClass extends Ctor {
getStyles() {
// ...
}
}
return StyledClass;
}
class SubClass extends withStyles(SuperClass) {
someMethod() {
this.someMethod()
}
}
请注意,withStyles 展示了一条特定的规则,那就是如果一个类(如 StyledClass)扩展一个泛型且受抽象构造函数(如 Ctor)限制的值,这个类也要声明为 abstract。这是因为我们无法知道是否传入了具有更多抽象成员的类,因此无法知道子类是否实现了所有抽象成员。
详情查看拉取请求:
https://github.com/microsoft/TypeScript/pull/36392
对于 TypeScript 用户来说,一个常见的场景是询问“为什么 TypeScript 包含了这个文件?”。推断程序文件是一个复杂的过程,因此很多情况下程序会使用 lib.d.ts 的某种组合、包含 node_modules 中的某些文件,或者在你以为 exclude 掉某些文件时依旧将它们包含在内。
tsc --explainFiles
# Forward output to a text file
tsc --explainFiles > expanation.txt
# Pipe output to a utility program like `less`, or an editor like VS Code
tsc --explainFiles | less
tsc --explainFiles | code -
TS_Compiler_Directory/4.2.0-beta/lib/lib.es5.d.ts
Library referenced via 'es5' from file 'TS_Compiler_Directory/4.2.0-beta/lib/lib.es2015.d.ts'
TS_Compiler_Directory/4.2.0-beta/lib/lib.es2015.d.ts
Library referenced via 'es2015' from file 'TS_Compiler_Directory/4.2.0-beta/lib/lib.es2016.d.ts'
TS_Compiler_Directory/4.2.0-beta/lib/lib.es2016.d.ts
Library referenced via 'es2016' from file 'TS_Compiler_Directory/4.2.0-beta/lib/lib.es2017.d.ts'
TS_Compiler_Directory/4.2.0-beta/lib/lib.es2017.d.ts
Library referenced via 'es2017' from file 'TS_Compiler_Directory/4.2.0-beta/lib/lib.es2018.d.ts'
TS_Compiler_Directory/4.2.0-beta/lib/lib.es2018.d.ts
Library referenced via 'es2018' from file 'TS_Compiler_Directory/4.2.0-beta/lib/lib.es2019.d.ts'
TS_Compiler_Directory/4.2.0-beta/lib/lib.es2019.d.ts
Library referenced via 'es2019' from file 'TS_Compiler_Directory/4.2.0-beta/lib/lib.es2020.d.ts'
TS_Compiler_Directory/4.2.0-beta/lib/lib.es2020.d.ts
Library referenced via 'es2020' from file 'TS_Compiler_Directory/4.2.0-beta/lib/lib.esnext.d.ts'
TS_Compiler_Directory/4.2.0-beta/lib/lib.esnext.d.ts
Library 'lib.esnext.d.ts' specified in compilerOptions
... More Library References...
foo.ts
Matched by include pattern '**/*' in 'tsconfig.json'
目前我们无法保证输出格式不变——它可能会随着时间而改变。如果你有任何建议,可以告诉我们帮助改进这个格式!
有关更多信息,请查看原始的拉取请求:
https://github.com/microsoft/TypeScript/pull/40011
const movieWatchCount: { [key: string]: number } = {};
function watchMovie(title: string) {
movieWatchCount[title] = (movieWatchCount[title] ?? 0) + 1;
}
type WesAndersonWatchCount = {
"Fantastic Mr. Fox"?: number;
"The Royal Tenenbaums"?: number;
"Moonrise Kingdom"?: number;
"The Grand Budapest Hotel"?: number;
};
declare const wesAndersonWatchCount: WesAndersonWatchCount;
const movieWatchCount: { [key: string]: number } = wesAndersonWatchCount;
// ~~~~~~~~~~~~~~~ error!
// Type 'WesAndersonWatchCount' is not assignable to type '{ [key: string]: number; }'.
// Property '"Fantastic Mr. Fox"' is incompatible with index signature.
// Type 'number | undefined' is not assignable to type 'number'.
// Type 'undefined' is not assignable to type 'number'. (2322)
type BatmanWatchCount = {
"Batman Begins": number | undefined;
"The Dark Knight": number | undefined;
"The Dark Knight Rises": number | undefined;
};
declare const batmanWatchCount: BatmanWatchCount;
// Still an error in TypeScript 4.2.
// `undefined` is only ignored when properties are marked optional.
const movieWatchCount: { [key: string]: number } = batmanWatchCount;
// Still an error in TypeScript 4.2.
// Index signatures don't implicitly allow explicit `undefined`.
movieWatchCount["It's the Great Pumpkin, Charlie Brown"] = undefined;
declare let sortOfArrayish: { [key: number]: string };
declare let numberKeys: { 42?: string };
// Error! Type '{ 42?: string | undefined; }' is not assignable to type '{ [key: number]: string; }'.
sortOfArrayish = numberKeys;
进一步了解请阅读原始拉取请求:
https://github.com/microsoft/TypeScript/pull/41921
感谢 Alexander Tarasyuk 提出的社区拉取请求,我们现在有了一个快速修复程序,用于基于调用站点声明新函数和方法!
https://github.com/microsoft/TypeScript/pull/41215
我们一直在努力减少新版中的重大更改数量。TypeScript 4.2 包含一些重大更改,但我们认为它们应该不会太影响升级过程。
const n: number = 123;
const s1 = `${n}px`; // `${number}px`
const s2: `${number}px` = s1;
const s3: `${number}pt` = s1; // Error
let v1 = s1; // string (because of widening)
这是一个突破,因为这些值过去仅有 string 类型。
更多信息参见相应的拉取请求:
https://github.com/microsoft/TypeScript/pull/41891
function* g1() {
const value = yield 1; // report implicit any error
}
function* g2() {
yield 1; // result is unused, no error
}
function* g3() {
const value: string = yield 1; // result is contextually typed by type annotation of `value`, no error.
}
function* g3(): Generator<number, void, string> {
const value = yield 1; // result is contextually typed by return-type annotation of `g3`, no error.
}
在相应的更改中查看更多信息:
https://github.com/microsoft/TypeScript/pull/41348
f (100)
(f < T) > (100)
如果你利用 TypeScript 的 API 来解析 JavaScript 文件中的类型构造,这可能会对你造成影响。尝试解析 Flow 文件时就可能出现这种情况。
"foo" in 42
// ~~
// error! The right-hand side of an 'in' expression must not be a primitive.
更多信息参见拉取请求:
https://github.com/microsoft/TypeScript/pull/41928
TypeScript 有一个接收 lift 函数的 visitNode 函数。现在,lift 需要一个 readonly Node[] 而不是 NodeArray
https://github.com/microsoft/TypeScript/pull/42000
我们热切希望听到你对 TypeScript 4.2 的看法!我们仍处于早期测试阶段,但我们希望你能提供宝贵的反馈意见,帮助打造更出色的版本。请立刻试用吧,如果遇到任何问题请告诉我们!
https://devblogs.microsoft.com/typescript/announcing-typescript-4-2-beta/