TypeScript 4.1 新特性

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 的 jsxjsxs 工厂函数:

  • 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,某些情况下赋值将会失败,而某些情况下的重载解析则将失败。解决方法是,最好使用类型断言来避免错误。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,163评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,301评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,089评论 0 352
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,093评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,110评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,079评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,005评论 3 417
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,840评论 0 273
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,278评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,497评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,667评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,394评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,980评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,628评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,796评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,649评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,548评论 2 352

推荐阅读更多精彩内容