高级 TypeScript

原文链接: 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 表达式。它允许我们使用两个或者更多类型(联合成员)去生成一个新的类型。

typescript 复制代码
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 方法,则会抛出错误。

交叉类型

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

typescript 复制代码
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 方法接受 Person 和 FootballPlayer 联合起来的类型。只有方法参数对象中都包含了 name,firstname 和 club 属性,参数对象才有效。

Keyof

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

typescript 复制代码
interface MovieCharacter {
  firstname: string,
  name: string,
  movie: string
}

type characterProps = keyof MovieCharacter;

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

typescript 复制代码
type characterProps = 'firstname' | 'name' | 'movie';

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

typescript 复制代码
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。在这个例子中,我们通过参数 menu,menuEntry 和 change 来调用 adjustMenu 函数,如上。

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

Typeof

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

typescript 复制代码
let firstname = 'Frodo';
let name: typeof firstname;

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

typescript 复制代码
function getCharacter() {
  return {
    firname: 'Frodo',
    name: 'Baggins'
  };
}

type Character = ReturnType<typeof getCharacter>;

/*
equal to 

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

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

条件类型

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

javascript 复制代码
condition ? returnTypeIfTrue : returnTypeIfFalse;

在 Typescript 中,该概念也存在。

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 的属性都变成可选的。

typescript 复制代码
interface MovieCharacter {
  firstname: string;
  name: string;
  movie: string;
}

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

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

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

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

Required(必须)

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

typescript 复制代码
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,第二个是要提取的类型。

typescript 复制代码
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(排除)

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

typescript 复制代码
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 中的 var,let 或者 const。

typescript 复制代码
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(映射类型)

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

typescript 复制代码
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 实用类型。

typescript 复制代码
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 属性,因为我们使用了 - 前缀。

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

typescript 复制代码
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 属性,且添加 ? 让每个属性可选。

typescript 复制代码
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 类型。

typescript 复制代码
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 实用类型如何?

typescript 复制代码
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 很棒,它提供了更多的特性。一旦我们掌握了本文描述的概念,我们可以让自己写的代码更加健壮并容易重构。

相关推荐
计算机魔术师13 分钟前
管谷歌的不是Pichai一个人,是一整层AI智能体
前端
米米商聊18 分钟前
图标能点,按钮就可用了吗?键盘、名称与收藏状态
前端·javascript
Younglina1 小时前
三个晚上,我和 WorkBuddy 做了个生图网站
前端·后端·ai编程
xy34531 小时前
Axure 9.0 内联框架加载方式
前端·ui·html·axure·原型·产品设计
技术小甜甜2 小时前
【Cursor】Skill 被标黄色警告,怎么改成合格的 SKILL.md
开发语言·javascript·ai编程·cursor·1024程序员节·ai agent·skills
陈随易2 小时前
副业搞钱点子之收入查看器
前端·后端
Profile排查笔记2 小时前
浏览器指纹逆向怎么分析:从网络请求追踪到 JavaScript 采集链
前端·人工智能·后端·自动化
hudie7653 小时前
LikeShop 二开最常改的 5 个文件(附改前改后对比)
前端·uni-app·php·个人开发
qetfw3 小时前
Bash 函数与流程控制:read、if、for、case 验证
linux·前端·javascript·bash
鬼手点金4 小时前
从 API 消息到模型 Token:Chat Template
java·前端·javascript·数据库·人工智能·深度学习·1024程序员节