TypeScript 4.1 新特性

Lasted 2020-12-04 14:07:46

模板字面量类型(Template Literal Types)

TypeScript 允许字符串字面量类型变量在函数或 APIS 中指定一组特定的字符串。

function setVerticalAlignment(color: "top" | "middle" | "bottom") {
  // ...
}

setVerticalAlignment("middel");
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
//   };

但是在另一个地方,字符串字面量类型可以用作构建块:构建其他字符串字面量类型。

这就是为什么 TypeScript 4.1 引入模板字符串字面量类型的原因。它具有与 JavaScript 中的模板文字字面量相同的语法,但用于类型位置。当将它与具体字面量类型一起使用时,它将通过串联内容来产生新的字符串字面量类型。

type World = "world";

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

当联合类型在子字符串中替换时会发生什么呢?它产生可以由每个联合成员表示的每个可能的字符串字面量的集合。以下两个变量运行出现四种可能的值。

type Color = "red" | "blue";
type Quantity = "one" | "two";

type SeussFish = `${Quantity | Color} fish`;
//   ^ = type SeussFish = "one fish" | "two fish" | "red fish" | "blue fish"

除发行说明中的​​示例外,还可以这样使用此功能。例如,用于 UI 组件的多个库提供了一种在其 API 中同时指定垂直和水平对齐方式的方法,通常同时使用单个字符串(例如“右下”)同时指定两者。在垂直对齐 “top”,“middle” 和 “ bottom” 以及水平对齐 “left”,“center” 和 “right” 之间,存在9种可能的字符串,其中每个前一个字符串与每个后面的字符串使用破折号。

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!
Argument of type '"top-middel"' is not assignable to parameter of type '"top-left" | "top-center" | "top-right" | "middle-left" | "middle-center" | "middle-right" | "bottom-left" | "bottom-center" | "bottom-right"'.
setAlignment("top-pot");    // error! but good doughnuts if you're ever in Seattle
Argument of type '"top-pot"' is not assignable to parameter of type '"top-left" | "top-center" | "top-right" | "middle-left" | "middle-center" | "middle-right" | "bottom-left" | "bottom-center" | "bottom-right"'.

尽管有很多此类 API 的示例,我们可以手动将其编写出来,这只是一个简单示例。实际上,对于9个字符串的示例相对比较简单。但是,当您需要大量字符串时,最好提前自动生成它们,以节省每次类型检查的工作(使用字符串,这将更容易理解)。

一些实际价值来自动态创建新的字符串文字。例如,有一个 makeWatchedObject API,它接受一个对象并克隆相同的对象,有一个 on 方法来检测属性的变化。

let person = makeWatchedObject({
  firstName: "Homer",
  age: 42, // give-or-take
  location: "Springfield",
});

person.on("firstNameChanged", () => {
  console.log(`firstName was changed!`);
});

on 侦听事件 “firstNameChanged”,而不仅仅是 “firstName”。我们将如何输入?

type PropEventSource = {
    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(obj: T): T & PropEventSource;

这样,当我们赋予错误的属性时,我们可以构建出错误的东西!

// error!
person.on("firstName", () => {});
Argument of type '"firstName"' is not assignable to parameter of type '"firstNameChanged" | "ageChanged" | "locationChanged"'.
// error!
person.on("frstNameChanged", () => {});
Argument of type '"frstNameChanged"' is not assignable to parameter of type '"firstNameChanged" | "ageChanged" | "locationChanged"'.

我们还可以用模板文字字面量做一些特殊的事情:我们可以从替换位置推断出来。下面示例从 eventName 字符串的各个部分进行推断,以找出关联的属性。

type PropEventSource = {
    on
        (eventName: `${K}Changed`, callback: (newValue: T[K]) => void ): void;
};

declare function makeWatchedObject(obj: T): T & PropEventSource;

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");
    }
})

在这里,我们介绍了一种通用方法。当使用字符串 “firstNameChanged” 进行调用时,TypeScript 会尝试推断 K 的正确类型。为此,它将 K 与 “Changed” 之前的内容进行匹配,并推断字符串 “firstName”。on 方法可以获取原始对象上的 firstName 类型,在这种情况下为字符串。类似地,当我们使用 “ageChanged” 调用时,它会找到属性 age 的类型(即number)。

推理可以以不同的方式组合,通常是对字符串进行解构,并以不同的方式对其进行重构。实际上,为了帮助修改这些字符串文字类型,我们添加了一些新的实用程序类型别名,用于修改字母中的大小写(即转换为小写和大写字符)。

type EnthusiasticGreeting = `${Uppercase}`

type HELLO = EnthusiasticGreeting<"hello">;
//   ^ = type HELLO = "HELLO"

新的类型别名为 Uppercase, Lowercase, Capitalize and Uncapitalize。前两个转换字符串中的每个字符,后两个仅转换字符串中的第一个字符。

映射类型中的键重映射(Key Remapping in Mapped Types)

映射类型可以基于任意键创建新的对象类型。

type Options = {
  [K in "noImplicitAny" | "strictNullChecks" | "strictFunctionTypes"]?: boolean;
};
// same as
//   type Options = {
//       noImplicitAny?: boolean,
//       strictNullChecks?: boolean,
//       strictFunctionTypes?: boolean
//   };

新的方法,基于其他对象类型的新对象类型。

/// 'Partial' is the same as 'T', but with each property marked optional.
type Partial = {
  [K in keyof T]?: T[K];
};

到目前为止,映射类型只能使用您提供的键来产生新的对象类型。但是,很多时候您希望能够根据输入来创建新键或过滤掉键。

因此,TypeScript 4.1允许您使用新的as子句重新映射映射类型中的键。

type MappedTypeWithNewKeys = {
    [K in keyof T as NewKeyType]: T[K]
    //            ^^^^^^^^^^^^^
    //            This is the new syntax!
}

使用这个新的as子句,您可以利用模板字面量类型之类的功能轻松地基于旧名称创建属性名称。

type Getters = {
    [K in keyof T as `get${Capitalize}`]: () => T[K]
};

interface Person {
    name: string;
    age: number;
    location: string;
}

type LazyPerson = Getters;
//   ^ = type LazyPerson = {
//       getName: () => string;
//       getAge: () => number;
//       getLocation: () => string;
//   }

可以通过 never 过滤 keys。这意味着在某些情况下,您不必使用额外的 Omit 程序类型。

// Remove the 'kind' property
type RemoveKindField = {
    [K in keyof T as Exclude]: T[K]
};

interface Circle {
    kind: "circle";
    radius: number;
}

type KindlessCircle = RemoveKindField;
//   ^ = type KindlessCircle = {
//       radius: number;
//   }

递归条件类型(Recursive Conditional Types)

例如,如果我们想编写一个类型来获取嵌套数组的元素类型,则可以编写以下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 ? 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>;

请记住,尽管这些递归类型功能强大,但应负责任地谨慎使用。

首先,这些类型可以完成很多工作,这意味着它们可以增加类型检查时间。尝试以Collat​​z猜想或斐波那契数列为模型建模可能很有趣,但不要将其放在npm的.d.ts文件中。

但是,除了计算量大之外,这些类型还可能在足够复杂的输入上达到内部递归深度限制。当达到该递归限制时,将导致编译时错误。通常,最好不要使用这些类型,而要写一些在更实际的示例中失败的东西。