高级 TypeScript

1,212 阅读7分钟

原文链接: Advanced TypeScript - 原文作者: kreuzercode

本文采用的是意译的方式

本文旨在熟悉些 TypeScript 的高级特性。

TypeScript 很优秀。它提供了很多很棒的特性。下面是一些不错的高级特性汇总。

  • 联合类型和交叉类型(Union and intersection types
  • Keyof
  • Typeof
  • 条件类型(Conditional types
  • 实用工具类型(Utility types
  • 推断类型(Infer type
  • 映射类型(Mapped types

联合类型和交叉类型

Typescript 允许我们结合多个类型来创建一个新的类型。这方法很像 JavaScript 中的逻辑表达式 OR || 或者 AND &&,我们使用它创建一个功能更强大的检查条件。

联合类型

联合类型很像 JavaScript 中的 OR 表达式。它允许我们使用两个或者更多类型(联合成员)去生成一个新的类型。

function orderProduct(orderId: string | number) {
  console.log('Ordering product with id ', orderId);
}

// 👍
orderProduct(1);

// 👍
orderProduct('123-abc');

// 👎 Argument is not assignable to string | number
orderProduct({ name: 'foo' });

我们使用联合类型为 orderProduct 方法添加类型。当我们以不是 number 或者 string 类型的参数调用 orderProduct 方法,则会抛出错误。

交叉类型

交叉类型,也就是说,整合多个类型为一个类型。这个新的类型拥有所有其他类型的特性。

interface Person {
  name: string,
  firstname: string
}

interface FootballPlayer {
  club: string;
}

function tranferPlayer(player: Person & FootballPlayer) {}

// 👍
transferPlayer({
  name: 'Ramos',
  firstname: 'Sergio',
  club: 'PSG',
});

// 👎 Argument is not assignable to Person & FootballPlayer
transferPlayer({
  name: 'Ramos',
  firstname: 'Sergio'
});

transferPlayer 方法接受 PersonFootballPlayer 联合起来的类型。只有方法参数对象中都包含了 namefirstnameclub 属性,参数对象才有效。

Keyof

现在,我们知道了联合类型。我们来看看 keyof 操作符。keyof 操作符会获取 interface 或者对象 object 的键,并产生一个联合类型。

interface MovieCharacter {
  firstname: string,
  name: string,
  movie: string
}

type characterProps = keyof MovieCharacter;

明白了!但是什么时候是有用处的呢?我们也可以直接给 characterProps 添加类型。

type characterProps = 'firstname' | 'name' | 'movie';

是的,我们可以这样做。keyof 让我们的代码更加健壮,并且始终保持我们的类型更新。我们来举个例子。

interface PizzaMenu {
  starter: string,
  pizza: string,
  beverage: string,
  dessert: string
}

const simpleMenu: PizzaMenu = {
  starter: 'Salad',
  pizza: 'Pepperoni',
  beverage: 'Coke',
  dessert: 'Vanilla ice cream',
};

function adjustMenu(
  menu: PizzaMenu,
  menuEntry: keyof PizzaMenu,
  change: string
) {
  menu[menuEntry] = change;
}

// 👍
adjustMenu(simpleMenu, 'pizza', 'Hawaii');
// 👍
adjustMenu(simpleMenu, 'beverage', 'Beer');

// 👎 Type - 'beverager' is not assignable
adjustMenu(simpleMenu, 'beverager', 'Beer');
// 👎 Wrong property - 'coffee' is not assignable
adjustMenu(simpleMenu, 'coffee', 'Beer');

函数 adjustMenu 允许你更改菜单 menu。比如,想象下你很喜欢菜单 menuSimple,但是你更喜欢喝啤酒 beer 而不是 Coke。在这个例子中,我们通过参数 menumenuEntrychange 来调用 adjustMenu 函数,如上。

这个函数很有趣的一部分是,menuEntry 是使用 keyof 运算符进行类型标注。这样,我们的代码就变得很健壮。如果我们重构 PizzaMenu 接口,我们就不需要改动 adjustMenu 函数。其会通过 PizzaMenu 的键进行更新。

Typeof

typeof 允许你从一个值中提取类型。它可以在类型上下文中使用,用来引用一个变量的类型。

let firstname = 'Frodo';
let name: typeof firstname;

当然,在这个场景中并没什么用处。但是,我们看看更加复杂的例子。在这个例子中,我们联合 ReturnType 使用 typeof 来提取函数返回的类型。

function getCharacter() {
  return {
    firname: 'Frodo',
    name: 'Baggins'
  };
}

type Character = ReturnType<typeof getCharacter>;

/*
equal to 

type Character = {
  firstname: string;
  name: string;
}
*/

在上面的例子中,我们基于 getCharacter 函数返回的类型,创建了一个新的类型。一样的,这里,我们更改了这个函数返回的类型,Character 类型随之更新。

条件类型

JavaScript 中,三元条件操作符我们很清楚。

condition ? returnTypeIfTrue : returnTypeIfFalse;

Typescript 中,该概念也存在。

interface StringId {
  id: string;
}

interface NumberId {
  id: number;
}

type Id<T> = T extends string ? StringId : NumberId;

let idOne: Id<string>;
// equal to let idOne: StringId;

let idTwo: Id<number>;
// equal to let idTwo: NumberId;

在这个例子中,我们基于 string 使用 Id 类型方法生成一个类型。如果 T 继承自 string,我们返回 StringId 类型。如果我们传递了一个 number,我们返回 NumberId 类型。

实用工具类型

实用工具类型是辅助工具,用于简化常用的类型转换。Typescript 提供了很多使用类型。本文中我们介绍了些。

在下面的链接中你可以找到更多 utility-types

Partial(部分/可选)

Partial 实用类型允许你将一个接口转换成另外一个接口,转换后的 interface 的属性都变成可选的。

interface MovieCharacter {
  firstname: string;
  name: string;
  movie: string;
}

function registerCharacter(character: Partial<MovieCharacter>) {}

// 👍
registerCharacter({
  firstname: 'Frodo',
});

// 👍
registerCharacter({
  firstname: 'Frodo',
  name: 'Baggins',
});

MovieCharacter 必传 firstnamenamemovie。但是,函数 registerCharacter 参数通过 Partial 实用方法转换为可选的 firstname,可选的 name 和可选的 movie 字段。

Required(必须)

RequiredPartial 相反。它将所有的可选属性的类型转换成必填属性的类型。

interface MovieCharacter {
  firstname?: string;
  name?: string;
  movie?: string;
}

function hireActor(character: Required<MovieCharacter>) {}

// 👍
hireActor({
  firstname: 'Frodo',
  name: 'Baggins',
  movie: 'The Lord of the Rings',
});

// 👎
hireActor({
  firstname: 'Frodo',
  name: 'Baggins',
});

Extract(提取/包含)

Extract 允许我们从一个类型中提取出你想要的信息。Extract 接收两个参数,第一个是 interface,第二个是要提取的类型。

type MovieCharacters = 
  | 'Harry Potter'
  | 'Tom Riddle'
  | { firstname: string; name: string };
  
type hpCharacters = Extract<MovieCharacters, string>;
// equal to type hpCharacters = 'Harry Potter' | 'Tom Riddle';

type hpCharacters = Extract<MovieCharacters, { firstname: string }>;
// equal to type hpCharacters = { firstname: string; name: string };

Extract<MovieCharacters, string> 创建了联合类型 hpCharacters,它由字符串构成。另一个 Extract<MovieCharacters, {firstname: string}> 提取了所有包含 firstname: string 类型。

Exclude(排除)

ExcludeExtract 相反。它允许我们通过排除类型来生成一个新的类型。

type MovieCharacters =
  | 'Harry Potter'
  | 'Tom Riddle'
  | { firstname: string; name: string };

type hpCharacters = Exclude<MovieCharacters, string>;
// equal to type hpCharacters = {firstname: string; name: string };

type hpCharacters = Exclude<MovieCharacters, { firstname: string }>;
// equal to type hpCharacters = 'Harry Potter' | 'Tom Riddle';

首先,我们通过排除 string 来生成一个新的类型。然后,我们又通过排除所有对象中包含 firstname: string 来生成一个新的类型。

Infer type(推断类型)

infer 允许我们创建一个新类型。类比 Javascript 中的 varlet 或者 const

type flattenArrayType<T> = T extends Array<infer ArrayType> ? ArrayType : T;

type foo = flattenArrayType<string[]>;
// equal to type foo = string;

type foo = flattenArrayType<number[]>;
// equal to type foo = number;

type foo = flattenArrayType<number>;
// equal to type foo = number;

哇,flattenArrayType 看起来很复杂。事实上并不是。我们一步步解析。

T extends Array<infer ArrayType> 检查 T 继承自一个数组。然后我们使用 infer 关键字获取到当前的数组类型。然后将其该类型存放在一个变量中。

然后,我们使用条件类型进行判断。如果 T 继承自数组,则返回数组类型,如果不是,则返回 T

Mapped types(映射类型)

映射类型是将已有的类型转换成新类型的一种很好的方式。因此有了 映射 这个术语。映射类型具有强大的功能,允许我们常见自定义实用工具类型。

interface Character {
  playInFantasyMovie: () => void;
  playInActionMovie: () => void;
}

type toFlags<Type> = { [Property in keyof Type]: boolean };

type characterFeature = toFlags<Character>;

/*

equal to 

type characterFeatures = {
  playInFantasyMovie: boolean;
  playInActionMovie: boolean;
}
*/

我们创建了 toFlag 辅助类型,该类型接受一个类型,并将所有的属性映射为返回类型为布尔值。

很不错。但是我们可以获取更强大功能。我们可以添加或者移除 ? 或者通过简单的前缀 + 或者 - 添加 readonly 修饰词。

看下面例子,我们创建了一个 mutable 实用类型。

type mutable<Type> = {
  -readonly [Property in keyof Type]: Type[Property];
};

type Character = {
  readonly firstname: string;
  readonly name: string;
};

type mutableCharacter = mutable<Character>;
/*

equal to

type mutableCharacter = {
  firstname: string;
  name: string;
}

 */

Character 类型中的每个属性都是只读的。我们的 mutable 接口移除了 readonly 属性,因为我们使用了 - 前缀。

同理。如果我们添加了 +,我们可以创建辅助类型,可以将接口转换成可选的属性,如下。

type optional<Type> = {
  [Property in keyof Type]+?: Type[Property];
};

type Character = {
  firstname: string;
  name: string;
};

type mutableCharacter = optional<Character>;

/* 

equal to

type mutableCharacter = {
  firstname?: string;
  name?: string;
}

*/

当然,这两种方法可以合并。我们看看下面这个 optionalAndMutable 类型,其移除 readonly 属性,且添加 ? 让每个属性可选。

type optionalAndMutable<Type> = {
  -readonly [Property in keyof Type]+?: Type[Property];
};

type Character = {
  readonly firstname: string;
  readonly name: string;
};

type mutableCharacter = optionalAndMutable<Character>;

/*

equal to

type mutableCharacter = {
  firstname?: string;
  name?: string;
}

 */

它甚至可变得更强大了。如下例子,我们将存在的类型转换成 setters 类型。

type setters<Type> = {
  [Property in keyof Type as `set${Capitalize<
    string & Property
  >}`]: () => Type[Property];
};

type Character = {
  firstname: string;
  name: string;
};

type character = setters<Character>;

/*

equal to

type character = {
  setFirstname: () => string;
  setName: () => string;
}

*/

这里没有任何限制。我们甚至可以重用我们目前看到的知识点。结合 Exclude 实用类型如何?

type nameOnly<Type> = {
  [Property in keyof Type as Exclude<Property, 'firstname'>]: Type[Property];
};

type Character = {
  firstname: string;
  name: string;
};

type character = nameOnly<Character>;

/*

equal to 

type character = {
  name: string;
}

*/

Typescript 很棒,它提供了更多的特性。一旦我们掌握了本文描述的概念,我们可以让自己写的代码更加健壮并容易重构。