写了这么多年 TypeScript,最大的感触就是它非常易于理解——特别是对于具有 Java 背景的人。 但是,在听说了 TypeScript 4.1(该语言最近的重大更新)的新闻之后,我还是为新鲜的特性感到惊奇。
我不认为我是个无知的例外。 在利用该新闻作为机会来深入了解类型系统的实际工作方式之后,我想与您分享新版本的令人兴奋的功能和变化,并提供关键字说明和许多神奇的示例。
如果你的 TypeScript 语言基础比较扎实,并且渴望学习高级功能,那么就开始吧。
新的语言特性
模板字面类型
自 ES6 开始,我们就可以通过模板字面量(Template Literals)的特性,用反引号来书写字符串,而不只是单引号或双引号:
- const message = `text`;
正如 Flavio Copes 所言,模板字面量提供了之前用引号写的字符串所不具备的特性:
- 定义多行字符串非常方便
- 可以轻松地进行变量和表达式的插值
- 可以用模板标签创建 DSL(Domain Specific Language,领域特定语言)
模板字面量类型和 JavaScript 中的模板字符串语法完全一致,只不过是用在类型定义里面:
- 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';
当我们定义了一个具体的字面量类型时,TypeScript 会通过拼接内容的方式产生新的字符串字面量类型。
键值对类型中键的重新映射(Key Remapping)
映射类型可以基于任意键创建新的对象类型。 字符串字面量可以用作映射类型中的属性名称:
- type Actions = { [K in 'showEdit' | 'showCopy' | 'showDelete']?: boolean; };
- // 等同于
- type Actions = {
- showEdit?: boolean,
- showCopy?: boolean,
- showDelete?: boolean
- };
如果你想创建新键或过滤掉键,TypeScript 4.1 允许你使用新的 as 子句重新映射映射类型中的键:
- type MappedTypeWithNewKeys<T> = {
- [K in keyof T as NewKeyType]: T[K]
- }
TypeScript Remapping KeysThe new as clause lets you leverage features like template literal types to easily create new property names based on old ones. Keys can be filtered by producing never so that you don’t have to use an extra Omit helper type in some cases: 通过使用新的 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;
- // }
TypeScript 利用带有 as 子句的模板文字类型 (source)
JSX 工厂函数
JSX 代表 JavaScript XML,它允许我们使用 JavaScript 编写 HTML 元素并将其放置在 DOM 中,而无需任何 createElement() 或 appendChild() 方法,例如:
- const greeting = <h4>Yes I can do it!</h4>;
- ReactDOM.render(greeting, document.getElementById('root'));
TypeScript 4.1 通过编译器选项 jsx 的两个新选项支持 React 17 的 jsx 和 jsxs 工厂函数:
- react-jsx
- react-jsxdev
“这些选项分别用于生产和开发编译。通常,一个选项可以扩展自另一个选项。” — TypeScript发版说明
以下是两个用于生产和开发的 TypeScript 配置文档的两个示例:
- // ./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"
- }
- }
如下图所示,TypeScript 4.1 支持在像 React 这样的 JSX 环境中进行类型检查:
递归条件类型
另一个新增功能是递归条件类型,它允许它们在分支中引用自己,从而能够更灵活地处理条件类型,使得编写递归类型别名更加容易。下面是一个使用 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>>;
但是应当注意的是,TypeScript 需要更多时间来进行递归类型的类型检查。Microsoft 警告,应以负责任的态度谨慎使用它们。
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();
- }
在这里,我们看到不是 path 以及 permissions 的属性应具有 string | number 类型:
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
如果您有一个 JavaScript 项目,正在其中使用 checkJs 选项检查 .js 文件中的错误,则还应该声明 allowJs 以允许编译 JavaScript 文件。而 TypeScript 4.1 中,默认情况下 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 的参数不再是可选类型
Promise 中 resolve 的参数不再是可选的,例如下面的代码:
- new Promise((resolve) => {
- doSomethingAsync(() => {
- doSomething();
- resolve();
- });
- });
这段代码在 TypeScript 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,某些情况下赋值将会失败,而某些情况下的重载解析则将失败。解决方法是,最好使用类型断言来避免错误。
最后一点想法
TypeScript 通过在运行代码之前捕获错误并提供修复程序来节省我们的时间。通过深入了解 TypeScript,我们可以更好地了解如何改善代码结构,并得到解决复杂问题的方案。希望本文能够帮助你探索类型系统,并使您的编程旅程更加精彩。
TypeScript 4.1 可以通过 NuGet 或 NPM 获取:
- npm install typescript
原文地址:https://mp.weixin.qq.com/s/QMYnlRDBqyLc-Sj_II0IpQ