TypeScript 最近各版本主要特性总结

(在人生的道路上,当你的期望一个个落空的时候,你也要坚定,要沉着。——朗费罗)

在这里插入图片描述

TypeScript

官网
在线运行TypeScript代码
第三方中文博客

特性

  1. typescript是javascript的超集,向javascript继承额外的编辑器,在开发阶段就能发现更多的错误
  2. typescript可以在javascript运行的任何地方运行,因为它最终依然会编译成javascript
  3. typescript能够更好的和编辑器继承,给出只能IDE提示,提高开发效率

TypeScript最近各大版本的主要特性总结

TypeScript版本更新记录
以下最近每个版本都举例3-4个常用特性,可能是重大更新,也可能是优化或者bug修复等。更详细的请直接查看官网。

5.0

2023 年 3 月 16 日

1. 装饰器(注解)由原来的实验性功能改为正式支持

原生写法

class Person {
    
    
    name: string;
    constructor(name: string) {
    
    
        this.name = name;
    }

    greet() {
    
    
        console.log("LOG: Entering method.");

        console.log(`Hello, my name is ${
      
      this.name}.`);

        console.log("LOG: Exiting method.")
    }
}

使用typescript5.0装饰器的写法

function loggedMethod(originalMethod: any, _context: any) {
    
    

    function replacementMethod(this: any, ...args: any[]) {
    
    
        console.log("LOG: Entering method.")
        const result = originalMethod.call(this, ...args);
        console.log("LOG: Exiting method.")
        return result;
    }

    return replacementMethod;
}
class Person {
    
    
    name: string;
    constructor(name: string) {
    
    
        this.name = name;
    }

    @loggedMethod
    greet() {
    
    
        console.log(`Hello, my name is ${
      
      this.name}.`);
    }
}

const p = new Person("Ron");
p.greet();

// Output:
//
//   LOG: Entering method.
//   Hello, my name is Ron.
//   LOG: Exiting method.

2. 新增const类型参数

在推断对象的类型时,ts 通常会选择一种通用的类型。例如,在这种情况下,会将names的类型推断为string []

type HasNames = {
    
     readonly names: string[] };
function getNamesExactly<T extends HasNames>(arg: T): T["names"] {
    
    
    return arg.names;
}

// Inferred type: string[]
const names = getNamesExactly({
    
     names: ["Alice", "Bob", "Eve"]});

推断成string []固然没有问题,但由于names是readonly的,而推断出的类型不是readonly的,这就会产生一些困扰。虽然我们可以通过添加as const来解决这个问题,就像这样:

// The type we wanted:
//    readonly ["Alice", "Bob", "Eve"]
// The type we got:
//    string[]
const names1 = getNamesExactly({
    
     names: ["Alice", "Bob", "Eve"]});

// Correctly gets what we wanted:
//    readonly ["Alice", "Bob", "Eve"]
const names2 = getNamesExactly({
    
     names: ["Alice", "Bob", "Eve"]} as const);

这可能很麻烦且容易忘记。在 TypeScript 5.0 中,您现在可以将const修饰符添加到类型参数声明中,以导致const-like 推理成为默认值:

type HasNames = {
    
     names: readonly string[] };
function getNamesExactly<const T extends HasNames>(arg: T): T["names"] {
    
    
//                       ^^^^^
    return arg.names;
}

// Inferred type: readonly ["Alice", "Bob", "Eve"]
// Note: Didn't need to write 'as const' here
const names = getNamesExactly({
    
     names: ["Alice", "Bob", "Eve"] });

3. 支持多个配置文件extends

当我们使用tsconfig.json管理多个项目时,很多情况下都会在一个tsconfig.json配置文件上进行配置扩展,比如下面这样:

{
    
    
    "extends": "../../../tsconfig.base.json",
    "compilerOptions": {
    
    
        "outDir": "../lib",
        // ...
    }
}

但是,在某些情况下,您可能希望从多个配置文件进行扩展。所以为了在此处提供更多灵活性,Typescript 5.0 现在允许继承多个文件。

// tsconfig.json
{
    
    
    "extends": ["./tsconfig1.json", "./tsconfig2.json"],
    "files": ["./index.ts"]
}

4. 所有枚举变为联合枚举

解决了部分场景下的问题

enum Letters {
    
    
    A = "a"
}
enum Numbers {
    
    
    one = 1,
    two = Letters.A
}

// 5.0 之前,这个语句是不会报错的,因为enum的成员都被错误地认为是number类型
// 5.0 之后,这个语句会报类型错误,
// Type 'Numbers' is not assignable to type 'number'.ts(2322)
const t: number = Numbers.two; 

详情见pull request

5. moduleResolution bundler

TypeScript 4.7为其和设置引入了node16和选项。这些选项的目的是更好地模拟 Node.js 中 ECMAScript 模块的精确查找规则;然而,这种模式有很多限制,其他工具并没有真正强制执行

// entry.mjs
import * as utils from "./utils";     // ❌ wrong - we need to include the file extension.

import * as utils from "./utils.mjs"; // ✅ works

为了模拟打包器的工作方式,TypeScript 现在引入了一种新策略:–moduleResolution bundler

{
    
    
    "compilerOptions": {
    
    
        "target": "esnext",
        "moduleResolution": "bundler"
    }
}

6. 支持export type

当 TypeScript 3.8 引入纯类型导入时,新语法不允许用于export * from "module"或export * as ns from "module"重新导出。TypeScript 5.0 添加了对这两种形式的支持:

// models/vehicles.ts
export class Spaceship {
    
    
  // ...
}

// models/index.ts
export type * as vehicles from "./vehicles";

// main.ts
import {
    
     vehicles } from "./models";

function takeASpaceship(s: vehicles.Spaceship) {
    
    
  // ✅ ok - `vehicles` only used in a type position
}

function makeASpaceship() {
    
    
  return new vehicles.Spaceship();
  //         ^^^^^^^^
  // 'vehicles' cannot be used as a value because it was exported using 'export type'.
}

性能优化

速度、内存和包大小优化,以下是相对于4.9的优化

material-ui build time 90%
TypeScript Compiler startup time 89%
Playwright build time 88%
TypeScript Compiler self-build time 87%
Outlook Web build time 82%
VS Code build time 80%
typescript npm Package Size 59%

4.9

2022 年 11 月 15 日

1. accessor 关键字支持

accessor 是 ECMAScript 中即将推出的一个类关键字,TypeScript 4.9 对它提供了支持。

class Person {
    
    
    accessor name: string;
 
    constructor(name: string) {
    
    
        this.name = name;
    }
}

accessor 关键字可以为该属性在运行时转换为一对 get 和 set 访问私有支持字段的访问器。

class Person {
    
    
    #__name: string;
 
    get name() {
    
    
        return this.#__name;
    }
    set name(value: string) {
    
    
        this.#__name = name;
    }
 
    constructor(name: string) {
    
    
        this.name = name;
    }
}

2. 优化了in的类型检查

日常开发中,会使用in对对象的属性或者方法进行判断,但有时被判断的对象类型是未知的,对于未知的类型,ts默认推断为unknown。如果使用in,则会推断为object,但因为ts不知道具体的对象属性,所以在类型检查时会出错。
比如以下代码中使用in对packageJSON的name字段进行判断时,就出错了

interface Context {
    
    
    packageJSON: unknown;
}

function tryGetPackageName(context: Context) {
    
    
    const packageJSON = context.packageJSON;
    // Check to see if we have an object.
    if (packageJSON && typeof packageJSON === "object") {
    
    
        // Check to see if it has a string name property.
        if ("name" in packageJSON && typeof packageJSON.name === "string") {
    
    
        //                                              ~~~~
        // error! Property 'name' does not exist on type 'object.
            return packageJSON.name;
        //                     ~~~~
        // error! Property 'name' does not exist on type 'object.
        }
    }

    return undefined;
}

为了解决此问题,ts对in的类型检查进行了优化,比如判断name时会自动推断为object & Record<“name”, unknown>。

interface Context {
    
    
    packageJSON: unknown;
}

function tryGetPackageName(context: Context): string | undefined {
    
    
    const packageJSON = context.packageJSON;
    // Check to see if we have an object.
    if (packageJSON && typeof packageJSON === "object") {
    
    
        // Check to see if it has a string name property.
        if ("name" in packageJSON && typeof packageJSON.name === "string") {
    
    
            // Just works!
            return packageJSON.name;
        }
    }

    return undefined;
}

3. 检查是否相等NaN

在过去开发者通常对NaN进行判断,但这往往会造成业务错误,因为NaN不等于任何值,NaN的类型却和数字相同。
所以在本版本对NaN做等于或者全等于的比较时会直接抛出错误,并建议开发者使用Number.isNaN。

4.8

2022 年 8 月 25 日

1. 优化unknown类型检查

过去对于unknown的推断,ts默认类型依然为unknown,这次优化,对真值的推断默认为object。这样就更有利于开发人员开发。

// 示例
function narrowUnknownishUnion(x: {
    
    } | null | undefined) {
    
    
    if (x) {
    
    
        x;  // {}
    }
    else {
    
    
        x;  // {} | null | undefined
    }
}
// 过去和现在的对比
function narrowUnknown(x: unknown) {
    
    
    if (x) {
    
    
        x;  // used to be 'unknown', now '{}'
    }
    else {
    
    
        x;  // unknown
    }
}

2. infer改进了模板字符串类型中类型的推断

// SomeNum used to be 'number'; now it's '100'.
type SomeNum = "100" extends `${
      
      infer U extends number}` ? U : never;

// SomeBigInt used to be 'bigint'; now it's '100n'.
type SomeBigInt = "100" extends `${
      
      infer U extends bigint}` ? U : never;

// SomeBool used to be 'boolean'; now it's 'true'.
type SomeBool = "true" extends `${
      
      infer U extends boolean}` ? U : never;

3. 比较数组和数组字面量提示错误

if (peopleAtHome === []) {
    
    
//  ~~~~~~~~~~~~~~~~~~~
// This condition will always return 'false' since JavaScript compares objects by reference, not value.
    console.log("here's where I lie, broken inside. </3")
    adoptAnimals();
}

4. 改进了绑定模式的推理

对于以下代码,ts 会从绑定模式中选取一个类型来进行更好的推断。

declare function chooseRandomly<T>(x: T, y: T): T;

let [a, b, c] = chooseRandomly([42, true, "hi!"], [0, false, "bye!"]);
//   ^  ^  ^
//   |  |  |
//   |  |  string
//   |  |
//   |  boolean
//   |
//   number

但对于以下代码,ts依然会出现以下推断。

declare function f<T>(x?: T): T;

let [x, y, z] = f();

这是有问题的,因为没有传入参数,在4.8以前,因为会推断成any,所以没有出错。现在优化了此问题,对于这种推断,ts会报错

4.7

2022 年 5 月 11 日

1. Node.js 中的 ECMAScript 模块支持

TypeScript 4.7 通过两个新module设置添加了此功能:node16和nodenext。

{
    
    
    "compilerOptions": {
    
    
        "module": "node16",
    }
}

假如有以下代码

// ./foo.ts
export function helper() {
    
    
    // ...
}

// ./bar.ts
import {
    
     helper } from "./foo"; // only works in CJS

helper();

此代码在 CommonJS 模块中有效,但在 ES 模块中会失败,因为相对导入路径需要使用扩展。因此,必须重写它以使用输出的扩展名foo.ts– 因此bar.ts必须从./foo.js.

// ./bar.ts
import {
    
     helper } from "./foo.js"; // works in ESM & CJS

helper();

但在4.7中,这些已经支持,不需要再进行手动更改。

2. 计算属性的控制流分析

比如以下代码

const key = Symbol();

const numberOrString = Math.random() < 0.5 ? 42 : "hello";

const obj = {
    
    
    [key]: numberOrString,
};

if (typeof obj[key] === "string") {
    
    
    let str = obj[key].toUpperCase();
}

在4.7之前typeof obj[key] === "string"这段代码是不成立的,会触发错误,现在4.7版本对比进行了优化,可以正常工作

3. 改进了对象和方法中的函数推断

比如以下代码可以正常工作

declare function f<T>(arg: {
    
    
    produce: (n: string) => T,
    consume: (x: T) => void }
): void;

// Works
f({
    
    
    produce: () => "hello",
    consume: x => x.toLowerCase()
});

// Works
f({
    
    
    produce: (n: string) => n,
    consume: x => x.toLowerCase(),
});

但这段代码却不可以

// Was an error, now works.
f({
    
    
    produce: n => n,
    consume: x => x.toLowerCase(),
});

// Was an error, now works.
f({
    
    
    produce: function () {
    
     return "hello"; },
    consume: x => x.toLowerCase(),
});

// Was an error, now works.
f({
    
    
    produce() {
    
     return "hello" },
    consume: x => x.toLowerCase(),
});

虽然定了类型为string,但因为ts没有进行更细粒度的推断而导致错误。
现在4.7对此问题进行了优化,可以正常工作

4. 自定义模块解析策略moduleSuffixes

TypeScript 4.7 现在支持moduleSuffixes自定义模块说明符查找方式的选项。

{
    
    
    "compilerOptions": {
    
    
        "moduleSuffixes": [".ios", ".native", ""]
    }
}

鉴于上述配置,如下所示的导入

import * as foo from "./foo";

将尝试查看相关文件./foo.ios.ts,./foo.native.ts最后./foo.ts。

4.6

2022 年 1 月 21 日

1. 更宽泛的super检查

在 JavaScript 类中,必须 super() 在引用 this. TypeScript 也强制执行这一点,尽管它在确保这一点方面有点过于严格 。 在 TypeScript 中,如果构造函数的包含类具有任何属性初始值设定项,则在构造函数的开头包含任何代码以前是错误的 。

class Base {
    
    
    // ...
}

class Derived extends Base {
    
    
    someProperty = true;

    constructor() {
    
    
        // error!
        // have to call 'super()' first because it needs to initialize 'someProperty'.
        doSomeStuff();
        super();
    }
}

TypeScript 4.6 现在在该检查方面更加宽松,并允许其他代码在super之前运行。

2. 参数的控制流分析

比如以下代码,当第一个参数是字符串“str”时,它的第二个参数是一个字符串,而当第一个自变量是字符串“num”时,第二个自变量是一个数字。

function func(...args: ["str", string] | ["num", number]) {
    
    
    // ...
}

在ts具有这种剩余参数的签名推断的情况下,ts现在可以缩小依赖于其他参数的参数。

type Func = (...args: ["a", number] | ["b", string]) => void;

const f1: Func = (kind, payload) => {
    
    
    if (kind === "a") {
    
    
        payload.toFixed();  // 'payload' narrowed to 'number'
    }
    if (kind === "b") {
    
    
        payload.toUpperCase();  // 'payload' narrowed to 'string'
    }
};

f1("a", 42);
f1("b", "hello");

3. 更准确的索引访问

在4.6之前val.toUpperCase()会报异常,现在4.6优化了这个问题,类型推断更加准确了。

interface TypeMap {
    
    
    "number": number;
    "string": string;
    "boolean": boolean;
}

type UnionRecord<P extends keyof TypeMap> = {
    
     [K in P]:
    {
    
    
        kind: K;
        v: TypeMap[K];
        f: (p: TypeMap[K]) => void;
    }
}[P];

function processRecord<K extends keyof TypeMap>(record: UnionRecord<K>) {
    
    
    record.f(record.v);
}

processRecord({
    
    
    kind: "string",
    v: "hello!",

    // 'val' used to implicitly have the type 'string | number | boolean',
    // but now is correctly inferred to just 'string'.
    f: val => {
    
    
        console.log(val.toUpperCase());
    }
})

4.5

2021 年 11 月 17 日

1. 新的高级类型Awaited并改进Promise

Awaited表示一个 Promise 的 resolve 值类型。
比如以下代码在过去可以使用infer条件类型来进行推断

const pro = Promise.resolve(Promise.resolve(Promise.resolve('hello world')))
type tPro = typeof pro extends Promise<infer U> ? U : typeof pro;

但写起来较为麻烦,现在有了Awaited之后,写法更简单明了。

const pro = Promise.resolve(Promise.resolve(Promise.resolve('hello world')))
type tPro = Awaited<typeof pro>

2. 支持node_modules中的lib

为确保 TypeScript 和 JavaScript 支持开箱即用,TypeScript 捆绑了一系列声明文件 ( .d.ts files)。这些声明文件代表 JavaScript 语言中可用的 API 和标准浏览器 DOM API。
不过,使用 TypeScript 包含这些声明文件偶尔会有两个缺点:

  1. 当您升级 TypeScript 时,您还必须处理对 TypeScript 内置声明文件的更改,当 DOM API 更改如此频繁时,这可能是一个挑战。
  2. 很难自定义这些文件来使您的需求与项目依赖项的需求相匹配(例如,如果您的依赖项声明它们使用 DOM API,您也可能被迫使用 DOM API)。

ts4.5现在支持指定自定义的lib包来覆盖默认的lib的内置方法

{
    
    
 "dependencies": {
    
    
    "@typescript/lib-dom": "npm:@types/web"
  }
}

3. 更准确的模板字符串类型检查

比如以下代码在过去会报错,现在4.5版本优化了此问题

export interface Success {
    
    
    type: `${
      
      string}Success`;
    body: string;
}

export interface Error {
    
    
    type: `${
      
      string}Error`;
    message: string;
}

export function handler(r: Success | Error) {
    
    
    if (r.type === "HttpSuccess") {
    
    
        // 'r' has type 'Success'
        let token = r.body;
    }
}

4. 支持私有变量的检查

ts 4.5现在支持最新的ECMA提案,支持最新的#私有变量类型检查

class Person {
    
    
    #name: string;
    constructor(name: string) {
    
    
        this.#name = name;
    }

    equals(other: unknown) {
    
    
        return other &&
            typeof other === "object" &&
            #name in other && // <- this is new!
            this.#name === other.#name;
    }
}

4.4

2021 年 8 月 12 日

1. 更准确的字符串别名和条件类型判断

字符串别名类型检查。比如以下代码,在4.4之前是编译错误的,但在4.4已经可以正常运行

function foo(arg: unknown) {
    
    
    const argIsString = typeof arg === "string";
    if (argIsString) {
    
    
        console.log(arg.toUpperCase());
        //              ~~~~~~~~~~~
        // Error! Property 'toUpperCase' does not exist on type 'unknown'.
    }
}

判别式类型检查,比如以下代码可以在4.4版本正常运行

type Shape =
    | {
    
     kind: "circle", radius: number }
    | {
    
     kind: "square", sideLength: number };

function area(shape: Shape): number {
    
    
    // Extract out the 'kind' field first.
    const {
    
     kind } = shape;

    if (kind === "circle") {
    
    
        // We know we have a circle here!
        return Math.PI * shape.radius ** 2;
    }
    else {
    
    
        // We know we're left with a square here!
        return shape.sideLength ** 2;
    }
}

2. 索引类型支持Symbol

interface Colors {
    
    
    [sym: symbol]: number;
}

const red = Symbol("red");
const green = Symbol("green");
const blue = Symbol("blue");

let colors: Colors = {
    
    };

colors[red] = 255;          // Assignment of a number is allowed
let redVal = colors[red];   // 'redVal' has the type 'number'

colors[blue] = "da ba dee"; // Error: Type 'string' is not assignable to type 'number'.

3. 类内部的static静态代码块

其实也是支持最新的ECMA提案,该优化解决了静态属性和方法只能在构造函数和外部进行初始化的问题。有了静态代码块后,就可以直接在类内部初始化静态代码。

class A {
    
    
  static x = 3;
  static y;
  static z;

  static {
    
    
    // this 为 class 自身
    try {
    
    
      const obj = foo(this.x);

      this.y = obj.y;
      this.z = obj.z;
    }
    catch {
    
    
      this.y = 0;
      this.z = 0;
    }
  }
}

4.3

2021 年 5 月 12 日

1. 允许指定set的写入类型

可以对类和接口的set方法指定参数类型

class Thing {
    
    
    #size = 0;

    get size(): number {
    
    
        return this.#size;
    }

    set size(value: string | number | boolean) {
    
    
        let num = Number(value);

        // Don't allow NaN and stuff.
        if (!Number.isFinite(num)) {
    
    
            this.#size = 0;
            return;
        }

        this.#size = num;
    }
}
// Now valid!
interface Thing {
    
    
    get size(): number
    set size(value: number | string | boolean);
}

2. 支持方法覆盖(重写)

ts 4.3版本中添加了override标记,标记该方法是子类重写父类的方法。

class SomeComponent {
    
    
    setVisible(value: boolean) {
    
    
        // ...
    }
}
class SpecializedComponent extends SomeComponent {
    
    
    override setVisible() {
    
    
}

3. 支持对方法和set,get进行私有化命名

class Foo {
    
    
    #someMethod() {
    
    
        //...
    }
    get #someValue() {
    
    
        return 100;
    }
    publicMethod() {
    
    
        // 可以使用
        // 可以在类内部访问私有命名成员。
        this.#someMethod();
        return this.#someValue;
    }
}
new Foo().#someMethod();
//        ~~~~~~~~~~~
// 错误!
// 属性 '#someMethod' 无法在类 'Foo' 外访问,因为它是私有的。
new Foo().#someValue;
//        ~~~~~~~~~~
// 错误!
// 属性 '#someValue' 无法在类 'Foo' 外访问,因为它是私有的。

4. 更准确的泛型类型检查

在4.3版本之前,以下代码类型检查会出错。在4.3版本中可以正常运行

function makeUnique<T, C extends Set<T> | T[]>(
  collection: C,
  comparer: (x: T, y: T) => number
): C {
    
    
  // 假设元素已经是唯一的
  if (collection instanceof Set) {
    
    
    return collection;
  }
  // 排序,然后去重
  collection.sort(comparer);
  //         ~~~~
  // 错误:属性 'sort' 不存在于类型 'C' 上。
  for (let i = 0; i < collection.length; i++) {
    
    
    //                           ~~~~~~
    // 错误: 属性 'length' 不存在于类型 'C' 上。
    let j = i;
    while (
      j < collection.length &&
      comparer(collection[i], collection[j + 1]) === 0
    ) {
    
    
      //             ~~~~~~
      // 错误: 属性 'length' 不存在于类型 'C' 上。
      //       ~~~~~~~~~~~~~  ~~~~~~~~~~~~~~~~~
      // 错误: 元素具有隐式的 'any' 类型,因为 'number' 类型的表达式不能用来索引 'Set<T> | T[]' 类型。
      j++;
    }
    collection.splice(i + 1, j - i);
    //         ~~~~~~
    // 错误: 属性 'splice' 不存在于类型 'C' 上。
  }
  return collection;
}

4.2

2021 年 2 月 23 日

1. 更智能的类型别名检查

比如代码,我们希望 ts 将 doStuff 的返回值类型显示为 BasicPrimitive | undefined,但是在4.2之前,它却显示成了 string | number | boolean | undefined 类型。

export type BasicPrimitive = number | string | boolean;
export function doStuff(value: BasicPrimitive) {
    
    
    if (Math.random() < 0.5) {
    
    
        return undefined;
    }
    return value;
}

2. 允许在元组前/中放置剩余元素

在4.2之前,只允许在元组末尾添加剩余参数

let e: [string, string, ...boolean[]];

现在4.2版本,允许在前/中位置添加剩余参数

let foo: [...string[], number];
let bar: [boolean, ...string[], boolean];

3. 更严格的in检查

在4.2之前如果in右边是非对象类属性,则只能在运行时抛出错误。
现在在编译期间就能够发现错误了。

"foo" in 42
//       ~~
// error! The right-hand side of an 'in' expression must not be a primitive.

4. 改进逻辑表达式中的未被调用函数检查

在 --strictNullChecks 模式下,下面的代码会产生错误。

function shouldDisplayElement(element: Element) {
    
    
    // ...
    return true;
}
function getVisibleItems(elements: Element[]) {
    
    
    return elements.filter((e) => shouldDisplayElement && e.children.length);
    //                          ~~~~~~~~~~~~~~~~~~~~
    // 该条件表达式永远返回 true,因为函数永远是定义了的。
    // 你是否想要调用它?
}

4.1

2020 年 11 月 19 日

1. 支持模板字符串类型

type World = "world";

type Greeting = `hello ${
      
      World}`;
// same as
//   type Greeting = "hello world";

2. 允许指定类型映射

在4.1之前,你只能输入指定字符串或自由类型进行映射。

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]
};

但在4.1版本,我们可以指定类型对象进行映射。

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;
//   };

3. 递归条件类型

在 JavaScript 中,可以在任意级别展平和构建容器类型的函数相当常见。例如,考虑.then()的实例上的方法Promise。.then(…)展开每个承诺,直到它找到一个不是“类似承诺”的值,并将该值传递给回调。s 上还有一种相对较新的flat方法Array,可以取多深的深度来展平。
就所有实际意图和目的而言,在 TypeScript 的类型系统中表达这一点是不可能的。虽然有黑客来实现这一点,但这些类型最终看起来非常不合理。

这就是为什么 TypeScript 4.1 放宽了对条件类型的一些限制——以便它们可以对这些模式进行建模。在 TypeScript 4.1 中,条件类型现在可以在其分支中立即引用自身,从而更容易编写递归类型别名。

例如,如果我们想写一个类型来获取嵌套数组的元素类型,我们可以写成下面的deepFlatten类型。

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]]]]);

同样,在 TypeScript 4.1 中,我们可以编写一个Awaited类型来深度解包Promises。

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>>;

4.0

2020 年 8 月 20 日

1. 可变参元组类型

在JavaScript中有一个函数concat,它接受两个数组或元组并将它们连接在一起构成一个新数组。

function concat(arr1, arr2) {
    
    
  return [...arr1, ...arr2];
}

再假设有一个tail函数,它接受一个数组或元组并返回除首个元素外的所有元素。

function tail(arg) {
    
    
  const [_, ...result] = arg;
  return result;
}

那么,我们如何在TypeScript中为这两个函数添加类型?
在旧版本的TypeScript中,对于concat函数我们能做的是编写一些函数重载签名。

function concat(arr1: [], arr2: []): [];
function concat<A>(arr1: [A], arr2: []): [A];
function concat<A, B>(arr1: [A, B], arr2: []): [A, B];
function concat<A, B, C>(arr1: [A, B, C], arr2: []): [A, B, C];
function concat<A, B, C, D>(arr1: [A, B, C, D], arr2: []): [A, B, C, D];
function concat<A, B, C, D, E>(arr1: [A, B, C, D, E], arr2: []): [A, B, C, D, E];
function concat<A, B, C, D, E, F>(arr1: [A, B, C, D, E, F], arr2: []): [A, B, C, D, E, F];)

在保持第二个数组为空的情况下,我们已经编写了七个重载签名。 接下来,让我们为arr2添加一个参数。

function concat<A2>(arr1: [], arr2: [A2]): [A2];
function concat<A1, A2>(arr1: [A1], arr2: [A2]): [A1, A2];
function concat<A1, B1, A2>(arr1: [A1, B1], arr2: [A2]): [A1, B1, A2];
function concat<A1, B1, C1, A2>(arr1: [A1, B1, C1], arr2: [A2]): [A1, B1, C1, A2];
function concat<A1, B1, C1, D1, A2>(arr1: [A1, B1, C1, D1], arr2: [A2]): [A1, B1, C1, D1, A2];
function concat<A1, B1, C1, D1, E1, A2>(arr1: [A1, B1, C1, D1, E1], arr2: [A2]): [A1, B1, C1, D1, E1, A2];
function concat<A1, B1, C1, D1, E1, F1, A2>(arr1: [A1, B1, C1, D1, E1, F1], arr2: [A2]): [A1, B1, C1, D1, E1, F1, A2];

这已经开始变得不合理了。 不巧的是,在给tail函数添加类型时也会遇到同样的问题。

在受尽了“重载的折磨”后,它依然没有完全解决我们的问题。 它只能针对已编写的重载给出正确的类型。 如果我们想要处理所有情况,则还需要提供一个如下的重载:

function concat<T, U>(arr1: T[], arr2: U[]): Array<T | U>;

但是这个重载签名没有反映出输入的长度,以及元组元素的顺序。
TypeScript 4.0带来了两项基础改动,还伴随着类型推断的改善,因此我们能够方便地添加类型。
第一个改动是展开元组类型的语法支持泛型。 这就是说,我们能够表示在元组和数组上的高阶操作,尽管我们不清楚它们的具体类型。 在实例化泛型展开时 当在这类元组上进行泛型展开实例化(或者使用实际类型参数进行替换)时,它们能够产生另一组数组和元组类型。
例如,我们可以像下面这样给tail函数添加类型,避免了“重载的折磨”。

function tail<T extends any[]>(arr: readonly [any, ...T]) {
    
    
    const [_ignored, ...rest] = arr;
    return rest;
}

const myTuple = [1, 2, 3, 4] as const;
const myArray = ["hello", "world"];

// type [2, 3, 4]
const r1 = tail(myTuple);

// type [2, 3, 4, ...string[]]
const r2 = tail([...myTuple, ...myArray] as const);

第二个改动是,剩余元素可以出现在元组中的任意位置上 - 不只是末尾位置!

type Strings = [string, string];
type Numbers = [number, number];

// [string, string, number, number, boolean]
type StrStrNumNumBool = [...Strings, ...Numbers, boolean];

2. 元组参数标记

以下是常规的元组入参示例

function  foo ( ... args : [ string ,  number ] ) : void  {
    
     
    // ... 
}
foo ( "你好" ,  42 ) ;  // 有效

foo ( "hello" ,  42 ,  true ) ;  // 错误
foo ( "你好" ) ;  // 错误

现在可以为元组做标记,更利于维护,提高可观赏性

type Range = [start: number, end: number];
type Foo = [first: number, second?: string, ...rest: any[]];

在这里插入图片描述

3. 显式声明类型

以下代码,类型推断中可能出现undeinfd。但实际过程中,Math.random至少是0,而0是一个有效的值,从而导致代码编译报错

class Square {
    
    
    sideLength;

    constructor(sideLength: number) {
    
    
        if (Math.random()) {
    
    
            this.sideLength = sideLength;
        }
    }

    get area() {
    
    
        return this.sideLength ** 2;
        //     ~~~~~~~~~~~~~~~
        // error! Object is possibly 'undefined'.
    }
}

在4.0可以使用!感叹号声明类型是存在的值,就能够正常编译了

class Square {
    
    
    // definite assignment assertion
    //        v
    sideLength!: number;
    //         ^^^^^^^^
    // type annotation

    constructor(sideLength: number) {
    
    
        this.initialize(sideLength)
    }

    initialize(sideLength: number) {
    
    
        this.sideLength = sideLength;
    }

    get area() {
    
    
        return this.sideLength ** 2;
    }
}

4. 转换为可选链

可选链一出来就受到广大开发者的热爱,为了提高大家开发和重构代码的效率,现在有工具可以进行自动转换。
在这里插入图片描述

猜你喜欢

转载自blog.csdn.net/qq_42427109/article/details/130533706