需要安装这个 RC 版的同学,可以通过 NuGet 获取:
https://www.nuget.org/packages/Microsoft.TypeScript.MSBuild
npm install typescript@rc
你还可以通过以下方式获得编辑器支持:
-
下载 Visual Studio 2019/2017
-
按 Visual Studio Code 和 Sublime Text 的指南操作。
在这个版本中我们提供了一些令人兴奋的新特性、新的检查标志、编辑器生产力更新和性能改进。
下面就来看看 4.1 为我们准备了哪些内容!
-
引入字符串模板类型
-
在映射类型中加入键重映射
-
递归条件类型
-
新增检查索引访问功能 --noUncheckedIndexedAccess
-
使用 path 启用路径映射时可以不指定 baseUrl
-
checkJs 现在默认意味着 allowJs,不再需要同时设置 checkJs 和 allowJs
-
支持 React 17 的 JSX 功能
-
JSDoc @see 标签的编辑器支持
-
重大更改
function setVerticalAlignment(color: "top" | "middle" | "bottom") {
// ...
}
setVerticalAlignment("middel");
// ~~~~~~~~
// error: Argument of type '"middel"' is not assignable to
// parameter of type '"top" | "middle" | "bottom"'.
这个特性很好用,因为字符串字面量类型可以对我们的字符串值进行基本的拼写检查。
type Options = {
[K in "noImplicitAny" | "strictNullChecks" | "strictFunctionTypes"]?: boolean
};
// same as
// type Options = {
// noImplicitAny?: boolean,
// strictNullChecks?: boolean,
// strictFunctionTypes?: boolean
// };
字符串字面量类型还可以用作另一种构建块:构建其他字符串字面量类型。
type World = "world";
type Greeting = `hello ${World}`;
// same as
// type Greeting = "hello world";
type Color = "red" | "blue";
type Quantity = "one" | "two";
type SeussFish = `${Quantity | Color} fish`;
// same as
// type SeussFish = "one fish" | "two fish"
// | "red fish" | "blue fish";5
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!
setAlignment("top-pot"); // error! but good doughnuts if you're ever in Seattle
虽然这类 API 可用的有很多,但我们可以手动把这些选项都写出来,所以这个例子还是偏玩具一些的。实际上,如果只有 9 个字符串可选那没什么大不了。但当你需要大量字符串时,应考虑提前自动生成它们,这样就用不着那么多类型检查了(或只使用 string,这更容易理解)。
let person = makeWatchedObject({
firstName: "Homer",
age: 42, // give-or-take
location: "Springfield",
});
person.on("firstNameChanged", () => {
console.log(`firstName was changed!`);
});
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>;
// error!
person.on("firstName", () => {
});
// error!
person.on("frstNameChanged", () => {
});
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");
}
})
在这里我们把 on 变成了一种通用方法。当用户使用字符串“firstNameChanged”进行调用时,TypeScript 会尝试推断 K 的正确类型。为此,它将 K 与“Changed”之前的内容进行匹配,并推断字符串“firstName”。当 TypeScript 推断出来后,on 方法可以获取原始对象上的 firstName 类型,在这里是 string。类似地,当我们使用“ageChanged”调用时,它会找到属性 age 的类型(即 number)。
type EnthusiasticGreeting<T extends string> = `${Uppercase<T>}`
type HELLO = EnthusiasticGreeting<"hello">;
// same as
// type HELLO = "HELLO";
新的类型别名为 Uppercase、Lowercase、Capitalize 和 Uncapitalize。前两个会转换字符串中的每个字符,后两个仅转换字符串中的第一个字符。
欲了解更多信息,请参见原始的拉取请求和进行中的拉取请求:
https://github.com/microsoft/TypeScript/pull/40336
https://github.com/microsoft/TypeScript/pull/40580
type Options = {
[K in "noImplicitAny" | "strictNullChecks" | "strictFunctionTypes"]?: boolean
};
// same as
// type Options = {
// noImplicitAny?: boolean,
// strictNullChecks?: boolean,
// strictFunctionTypes?: boolean
// };
/// 'Partial<T>' is the same as 'T', but with each property marked optional.
type Partial<T> = {
[K in keyof T]?: T[K]
};
以前,映射类型只能使用你提供的键来生成新的对象类型。但很多时候你希望能够根据输入来创建新键或过滤掉键。
type MappedTypeWithNewKeys<T> = {
[K in keyof T as NewKeyType]: T[K]
// ^^^^^^^^^^^^^
// This is the new syntax!
}
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>;
// 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>;
// same as
// type KindlessCircle = {
// radius: number;
// };
欲了解更多信息,请查看 GitHub 上的原始拉取请求:
https://github.com/microsoft/TypeScript/pull/40336
在 JavaScript 中,经常能看到可以展开(flatten)并建立任意级别容器类型的函数。例如,考虑 Promise 实例上的.then() 方法。.then(...) 一个个展开 promise,直到它找到一个“不像 promise”的值,然后将该值传递给一个回调。Arrays 上还有一个相对较新的 flat 方法,从中可以看出展开的深度能有多大。
以前,处于各种实际因素,在 TypeScript 的类型系统中无法表达这一点。尽管有一些破解方法可以实现它,但最后出来的类型看起来会很奇怪。
所以 TypeScript 4.1 放宽了对条件类型的一些限制——以便它们可以构建这些模式。在 TypeScript 4.1 中,条件类型现在可以立即在其分支中引用自身,这样我们就更容易编写递归类型别名了。
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]]]]);
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>>;
请记住,尽管这些递归类型都很强大,但使用它们的时候应该小心谨慎。
首先,这些类型可以完成很多工作,这意味着它们会增加类型检查时间。用它计算 Collatz 猜想或斐波那契数列中的数字可能很有意思,但不要放在 npm 的.d.ts 文件里。
除了计算量大之外,这些类型还可能在足够复杂的输入上触及内部递归深度上限。达到这一递归上限时将导致编译时错误。一般来说最好不要使用这些类型,避免写出一些在更实际的场景中会失败的代码。
实现细节见此:
https://github.com/microsoft/TypeScript/pull/40002
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();
}
在上面的示例中,Options 有一个索引签名,其含义是任何尚未列出的 accessed 属性都应具有 string | number 类型。理想情况下(代码假定你知道自己在干什么)这很方便,但事实是,JavaScript 中的大多数值并不能完整支持所有潜在的属性名称。例如,大多数类型都不会像前面的示例那样,有一个 Math.random() 创建的属性键的值。对于许多用户而言,这种行为是超乎预料的,并且会感觉它没有充分利用 --strictNullChecks 的严格检查。
// 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();
function screamLines(strs: string[]) {
// this will have issues
for (let i = 0; i < strs.length; i++) {
console.log(strs[i].toUpperCase());
// ~~~~~~~
// error! Object is possibly 'undefined'.
}
}
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());
});
}
捕获越界错误时这个标志可能很方便,但它对于很多代码来说可能显得很累赘,因此 --strict 标志不会自动启用它。但如果你对这个特性很感兴趣,也可以随意尝试它,看它是否适合你团队的代码库!
欲了解更多信息,请查看实现的拉取请求:
https://github.com/microsoft/TypeScript/pull/39560
路径映射是相当常用的,通常是为了更好地导入,或者为了模拟 monorepo 链接行为。
不幸的是,指定 paths 来启用路径映射时,还需要指定一个名为 baseUrl 的选项,该选项也允许到达相对于 baseUrl 的 bare specifier paths。它还经常会使自动导入使用较差的路径。
在 TypeScript 4.1 中,可以在没有 baseUrl 的情况下使用 path 选项,从而避免其中一些问题。
以前,如果你要启动一个 checked 的 JavaScript 项目,则必须同时设置 allowJs 和 checkJs。这有点烦人,因此现在 checkJs 默认隐含了 allowJs。
欲了解更多信息,请查看拉取请求:
https://github.com/microsoft/TypeScript/pull/40275
TypeScript 4.1 通过 jsx 编译器选项的两个新选项,支持了 React 17 即将推出的 jsx 和 jsxs 工厂函数:
-
react-jsx
-
react-jsxdev
// ./src/tsconfig.json
{
"compilerOptions": {
"module": "esnext",
"target": "es2015",
"jsx": "react-jsx",
"strict": true
},
"include": [
"./**/*"
]
}
// ./src/tsconfig.dev.json
{
"extends": "./tsconfig.json",
"compilerOptions": {
"jsx": "react-jsxdev"
}
}
欲了解更多信息,请查看相应的 PR:
https://github.com/microsoft/TypeScript/pull/39199
// @filename: first.ts
export class C { }
// @filename: main.ts
import * as first from './first';
/**
* @see first.C
*/
function related() { }
感谢积极贡献者 Wenlu Wang 实现它:
https://github.com/microsoft/TypeScript/pull/39760
标记为 abstract 的成员不能再标记为 async。此处的解决方法是移除 async 关键字,因为调用方只关心返回类型。
以前,对于像 foo && somethingElse 这样的表达式,foo 的类型是 any 或 unknown 的,整个表达式的类型将是 somethingElse 的类型。
declare let foo: unknown;
declare let somethingElse: { someProp: string };
let x = foo && somethingElse;
但在 TypeScript 4.1 中,我们会更谨慎地确定这种类型。由于对 && 左侧的类型一无所知,因此我们将向外传播 any 和 unknown,而不是将右侧的类型传播出去。
function isThing(x: any): boolean {
return x && typeof x === 'object' && x.blah === 'foo';
}
一般来说,合适的解决方法是从 foo && someExpression 切换到!!foo && someExpression。
在 JavaScript 中,对象 spread(例如{ ...foo })不会对虚假值起作用。因此,在类似{ ...foo }的代码中,如果 foo 为 null 或 undefined,则会跳过 foo。
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 的属性将被 spread 进去;否则,不会将任何属性 spread 到返回的对象中。
{ x: number } | { x: number, name: string, age: number, location: string }
这个操作是这样的:如果定义了 pet,Person 的所有属性都将存在;否则,所有属性都不会在结果上定义。要么全有,要么都没有。但有人把这种模式用得太过分了,在单个对象中塞几百个 spread,每个 spread 都可能添加数百或数千个属性。事实证明,由于各种原因,这种做法的成本最后会飞天,并且往往不会带来太多收益。
{
x: number;
name?: string;
age?: number;
location?: string;
}
这样性能和代码简洁程度都会上一个台阶。
欲了解更多信息,请参见原始更改:
https://github.com/microsoft/TypeScript/pull/40778
// @filename: projectRoot/index.ts
export * from "./nested/base";
// @filename: projectRoot/nested/base.ts
export const a = "123"
declare module "nested/base" {
export const a = "123";
}
declare module "index" {
export * from "nested/base";
}
declare module "hello/nested/base" {
export const a = "123";
}
declare module "hello" {
export * from "hello/nested/base";
}
The `bundledPackageName` option must be provided when using outFile and node module resolution with declaration emit.
new Promise(resolve => {
doSomethingAsync(() => {
doSomething();
resolve();
})
})
resolve()
~~~~~~~~~
error TS2554: Expected 1 arguments, but got 0.
An argument for 'value' was not provided.
new Promise<number>(resolve => {
// ^^^^^^^^
doSomethingAsync(value => {
doSomething();
resolve(value);
// ^^^^^
})
})
new Promise<void>(resolve => {
// ^^^^^^
doSomethingAsync(() => {
doSomething();
resolve();
})
})
TypeScript 4.1 附带了一个快速修复以帮助解决这个问题。
在接下来的几周内,我们将密切注意 TypeScript 4.1 的稳定版本中需要包含的所有高优先级修复。如果可以的话,请试试我们的 RC 版本,帮助我们找出各种潜在问题。我们一直在努力改善大家的 TypeScript 体验!
编程快乐!
延伸阅读
https://devblogs.microsoft.com/typescript/announcing-typescript-4-1-rc/#breaking-changes
文章评论