TypeScript 4.1 新特性
新的语言特性
模版字面量类型
type Entity = 'Invoice';
type Notification = `${Entity} saved`;
// 等同于
// type Notification = 'Invoice saved'; // 是一个类型 🐂
type Viewport = 'md' | 'xs';
type Device = 'mobile' | 'desktop';
type Screen = `${Viewport | Device} screen`;
// 等同于下面这一行
// type Screen = 'md screen' | 'xs screen' | 'mobile screen' | 'desktop screen';
键值对类型中键的重新映射(Key Remapping)
TypeScript 4.1 允许你使用新的 as 子句重新映射映射类型中的键
通过使用新的 as 子句,我们可以利用模板字面量类型之类的特性轻松地基于旧属性创建新属性名称。我们可以通过输出 never 来过滤键,这样在某些情况下就不必使用额外的 Omit 辅助类型:
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>;
// ^ = type LazyPerson = {
// getName: () => string;
// getAge: () => number;
// getLocation: () => string;
// }
// 去掉 'kind' 属性
type RemoveKindField<T> = {
[K in keyof T as Exclude<K, "kind">]: T[K]
};
interface Circle {
kind: "circle";
radius: number;
}
type KindlessCircle = RemoveKindField<Circle>;
// ^ = type KindlessCircle = {
// radius: number;
// }
JSX 工厂函数
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"
}
}
递归条件类型
另一个新增功能是递归条件类型,它允许它们在分支中引用自己,从而能够更灵活地处理条件类型,使得编写递归类型别名更加容易。下面是一个使用 Awaited
展开深层嵌套的 Promise
的示例
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>>;
Checked indexed accesses 索引访问检查
TypeScript 中的索引签名允许可以像下面的 Options
接口中那样访问任意命名的属性
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
// 这些都可以!因为类型都是 string | number
opts.yadda.toString();
opts["foo bar baz"].toString();
opts[Math.random()].toString();
}
TypeScript 4.1 提供了一个新的标志 --noUncheckedIndexedAccess,使得每次属性访问(如 opts.path)或索引访问(如 opts [“ blabla”] )都可能未定义。这意味着如果我们需要访问上一个示例中的 opts.path 之类的属性,则必须检查其是否存在或使用非 null 断言运算符(后缀 ! 字符):
function checkOptions(opts: Options) {
opts.path; // string
opts.permissions; // number
// 以下代码在 noUncheckedIndexedAccess 开启时是非法的
opts.yadda.toString();
opts["foo bar baz"].toString();
opts[Math.random()].toString();
// 检查属性是否真的存在
if (opts.yadda) {
console.log(opts.yadda.toString());
}
// 直接使用非空断言操作符
opts.yadda!.toString();
}
--noUncheckedIndexedAccess 标志对于捕获很多错误很有用,但是对于很多代码来说可能很嘈杂。 这就是为什么 --strict 开关不会自动启用它的原因。
不需要 baseUrl 指定路径
在 TypeScript 4.1 之前,要能够使用 tsconfig.json 文件中的 paths,必须声明 baseUrl 参数。 在新版本中,可以在不带 paths 选项的情况下指定 baseUrl。 这解决了自动导入中路径不畅的问题。
{
"compilerOptions": {
"baseUrl": "./src",
"paths": {
"@shared": ["@shared/"] // This mapping is relative to "baseUrl"
}
}
}
checkJs 默认打开 allowJs
{
compilerOptions: {
allowJs: true,
checkJs: true
}
}
JSDoc @see 标签的编辑器支持
在编辑器中使用 TypeScript 时,现在对 JSDoc 标签 @see 有了更好的支持,这将改善 TypeScript 4.1 的可用性
// @filename: first.ts
export class C {}
// @filename: main.ts
import * as first from "./first";
/**
* @see first.C
*/
function related() {}
不兼容改变
lib.d.ts 变动
结构和 DOM 的环境声明,使您可以轻松地开始编写经过类型检查的 JavaScript 代码。
该文件自动包含在 TypeScript 项目的编译上下文中。 您可以通过指定 --noLib 编译器命令行标志或在 tsconfig.json 中配置 noLib 为 true 来排除它。
在 TypeScript 4.1 中,由于 DOM 类型是自动生成的,lib.d.ts 可能具有一组变动的 API,例如,从 ES2016 中删除的 Reflect.enumerate。
abstract 成员不能被标记为 async
在另一个重大更改中,标记为 abstract 的成员不能被再标记为 async。 因此,要修复您的代码,必须删除 async 关键字:
abstract class MyClass {
// 在 TypeScript 4.1 中必须删除 async
abstract async create(): Promise<string>;
}
any/unknown 向外传播
在 TypeScript 4.1 之前,对于像 foo && somethingElse 这样的表达式, foo 的类型是 any 或 unknown。 整个表达式的类型将是 somethingElse 的类型,在以下示例中就是 {someProp:string} :
declare let foo: unknown;
declare let somethingElse: { someProp: string };
let x = foo && somethingElse;
在 TypeScript 4.1 中, any 和 unknown 都将向外传播,而不是在右侧传播。通常,这个变更合适的解决方法是从 foo && someExpression 切换到 !!foo && someExpression。
注意:双重感叹号(!!)是将变量强制转换为布尔值(真或假)的一种简便方法。
Promise 中 resolve 的参数不再是可选类型
new Promise((resolve) => {
doSomethingAsync(() => {
doSomething();
resolve();
});
});
在 4.1 中编译会报错
resolve()
~~~~~~~~~
error TS2554: Expected 1 arguments, but got 0.
An argument for 'value' was not provided.
要解决这个问题,必须在 Promise 中给 resolve 提供至少一个值,否则,在确实需要不带参数的情况下调用 resolve() 的情况下,必须使用显式的 void 泛型类型参数声明 Promise:
new Promise<void>((resolve) => {
doSomethingAsync(() => {
doSomething();
resolve();
});
});
条件展开将会创建可选属性
在 JavaScript 中,展开运算符 { ...files } 不会作用于假值,例如 files 为 null 或者 undefined。
在以下使用条件传播的示例中,如果定义了 file,则将传播 file.owner 的属性。否则,不会将任何属性传播到返回的对象中:
function getOwner(file?: File) {
return {
...file?.owner,
defaultUserId: 123,
};
}
在 TypeScript 4.1 之前, getOwner 返回基于每个展开对象的联合类型:
{ x: number } | { x: number, name: string, age: number, location: string }
如果定义了 file,则会拥有来自 Person(所有者的类型)的所有属性。
否则,结果中一个都不会展示
但是事实证明,这样的代价最终会变得非常高昂,而且通常无济于事。在单个对象中存在数百个展开对象,每个展开对象都可能增加数百或数千个属性。 为了更好的性能,在 TypeScript 4.1 中,返回的类型有时使用全部可选属性:
{
x: number;
name?: string;
age?: number;
location?: string;
}
不匹配的参数将不再关联
过去,彼此不对应的参数在 TypeScript 中通过将它们与 any 类型关联而彼此关联。
在下面的重载示例(为同一功能提供多种功能类型)中, pickCard 函数将根据用户传入的内容返回两个不同的内容。如果用户传入表示 deck 的对象,则该函数将选择 card。 如果用户选择了 card,他们将得到他们选择的 card:
let suits = ["hearts", "spades", "clubs", "diamonds"];
function pickCard(x: { suit: string; card: number }[]): number;
function pickCard(x: number): { suit: string; card: number };
function pickCard(x: any): any {
// Check to see if we're working with an object/array
// if so, they gave us the deck and we'll pick the card
if (typeof x == "object") {
let pickedCard = Math.floor(Math.random() * x.length);
return pickedCard;
}
// Otherwise just let them pick the card
else if (typeof x == "number") {
let pickedSuit = Math.floor(x / 13);
return { suit: suits[pickedSuit], card: x % 13 };
}
}
let myDeck = [
{ suit: "diamonds", card: 2 },
{ suit: "spades", card: 10 },
{ suit: "hearts", card: 4 },
];
let pickedCard1 = myDeck[pickCard(myDeck)];
alert("card: " + pickedCard1.card + " of " + pickedCard1.suit);
let pickedCard2 = pickCard(15);
alert("card: " + pickedCard2.card + " of " + pickedCard2.suit);
使用 TypeScript 4.1,某些情况下赋值将会失败,而某些情况下的重载解析则将失败。解决方法是,最好使用类型断言来避免错误。