jiayisheji / blog

没事写写文章,喜欢的话请点star,想订阅点watch,千万别fork!
https://jiayisheji.github.io/blog/
505 stars 49 forks source link

学习 TypeScript 中的内置实用工具类型 #46

Open jiayisheji opened 2 years ago

jiayisheji commented 2 years ago

TypeScript 对于许多 Javascript 开发人员来说是难以理解的。引起麻烦的一个领域是高级类型。这个领域的一部分是 TypeScript 中内置的实用程序类型。它们可以帮助我们从现有类型中创建新的类型。在本文中,我们将了解其中一些实用工具类型如何工作以及如何使用它们。

实用工具类型简要介绍

TypeScript 为处理类型提供了一个强大的系统。这里有一些基本类型我们已经从 JavaScript 中了解。例如,数据类型如 numberstringbooleanobejctsymbolnullundefined。这并不是 TypeScript 提供的所有功能。在这些类型之上还有一些内置实用工具类型。

有时候,这些实用工具类型也是最难以理解的。当初次看到这些类型时尤为明显。好消息是,如果你理解一个重要的事情,这些类型实际上并不困难。

所有这些内置实用工具类型实际上都是简单的函数,能看这篇文章,说明你已经从 JavaScript 中知道的函数。这里的主要区别是,工具函数处理业务,这些实用工具类型,只处理类型。这些工具类型所做的就是将类型从一种类型转换为另一种类型。

这个输入是开始时使用的某种类型。还可以提供多种类型。接下来,该工具类型将转换该输入并返回适当的输出。所发生的转换类型取决于使用的实用工具类型。

Typescipt 内置了 16 个工具类型 和 4 个字符串类型(只能在字符串中使用,这里暂时不介绍它们),接下来我们就来:

Let's learn them one by one!

关于语法

TypeScript 中的所有实用工具类型都使用类似的语法。这将使我们更容易学习和记住它。如果我们尝试将这些类型看作类似于函数的东西,也会使它更容易。这通常有助于更快地理解语法,有时要快得多。关于语法。

每个实用工具类型都以类型的名称开始。这个名字总是以大写字母开头。名称后面是左尖和右尖的尖括号,小于和大于符号(<>)。括号之间是参数。这些参数是我们正在使用的类型,即输入。Typescipt 把这种语法叫泛型 GenericType<SpecificType>

仔细想想,使用实用程序类型就像调用一个函数并传递一些东西作为参数。这里的一个不同之处在于该函数始终以大写字母开头。第二个区别是,在函数名之后没有使用圆括号,而是使用尖括号。函数调用:fn(a, b)

有些类型需要一个参数,有些则需要两个或者更多。与 JavaScript 函数参数类似,这些参数由冒号(,)分割,并在尖括号之间传递。下面这个简单的例子说明了普通函数和 TypeScript 实用工具类型之间的相似性。

// 在JavaScript中调用函数
myFunction('one argument');
myFunction('one argument', 'two argument');
myFunction('one argument', 'some argument');

// 在TypeScript中使用内置类型
UtilityType<'one type'>;
UtilityType<'one type', 'two type'>;
UtilityType<'one type', 'some type'>;

关于可用性

我们将要学习的类型在 TypeScript 4.0 及以上版本中全部可用。确保你使用这个版本。否则,下面的一些类型可能无法工作,或者没有一些额外的包就无法工作。

Partial

创建 typeinterface 时,所有在内部定义的类型都需要作为默认值。如果我们想将某些标记为可选的,我们可以使用 ? 并将其放在属性名称之后。这将使该属性成为可选的。

// 一个可选的年龄的用户接口
interface Person {
    name: string;
    age?: number;
}
// 创建一个用户
const user: Person = {
    name: 'jack'
}

如果希望所有属性都是可选的,那么必须将所有属性都加上 ?。我们可以这样做:

// 一个可选的年龄的用户接口
interface Person {
    name?: string;
    age?: number;
}
// 创建一个用户
const user: Person = {}

它也会对与该 interface 一起工作的一切产生影响。我们可以使用的另一个选项是 Partial<Type>。该类型接受一个参数,即希望设置为可选的类型。它返回相同的类型,但其中所有先前必需的属性现在都是可选的。

// 创建一个接口
interface Person {
  name: string;
  age: number;
  jobTitle: string;
  hobbies: string[];
}

// 使用 Person 接口创建对象
const jack: Person = {
  name: 'Jack',
  age: 33,
  jobTitle: 'CTO',
  hobbies: ['reading']
}
// 这是 ok,因为 “jack”对象包含在 Person 接口中指定的所有属性。

// 使用 Person 接口创建新对象
const lucy: Person = {
  name: 'Lucy',
  age: 18,
}
// TS error: Type '{ name: string; age: number; }' is missing the following properties from type 'Person': jobTitle, hobbies

// 使用 Partial<Type> 和 Person 接口使 Person 接口中的所有属性都是可选的
const lucy: Partial<Person> = {
  name: 'Lucy',
  age: 18,
}

// 这也会有效: const alan: Partial = {}

// Partial 之后的 Person 接口: // interface Person { // name?: string; // age?: number; // jobTitle?: string; // hobbies?: string[]; // }

Required

Required<Type>Partial<Type> 正好相反。如果 Partial<Type> 使所有属性都是可选的,则 Required<Type> 使它们都是必需的、不可选的。Required<Type> 的语法与 Partial<Type> 相同。唯一的区别是实用工具类型的名称。

// 创建一个接口
interface Cat {
  name: string;
  age: number;
  hairColor: string; 
  owner?: string; // <= 使“owner”属性可选
}

// 这将有效:
const suzzy: Cat = {
  name: 'Suzzy',
  age: 2,
  hairColor: 'white',
}

// 使用 Required<Type> 连同 Cat 接口使 Cat 接口中的所有属性都是必需的:
const suzzy: Required<Cat> = {
  name: 'Suzzy',
  age: 2,
  hairColor: 'white',
}
// TS error: Property 'owner' is missing in type '{ name: string; age: number; hairColor: string; }' but required in type 'Required<Cat>'.

// Required<Cat> 之后的 Cat 接口:
// interface Cat {
//   name: string;
//   age: number;
//   hairColor: string;
//   owner: string;
// }

Readonly

有时我们希望使某些数据不可变,防止他们被修改了。Readonly<Type> 类型可以帮助我们对整个类型进行这种更改。例如,可以将接口中的所有属性设置为只读。当我们在某个对象中使用该接口,并试图改变某个对象的属性时,TypeScript 会抛出一个错误。

// 创建一个接口:
interface Book {
  title: string;
  author: string;
  numOfPages: number;
}

// 创建一个使用 Book 接口的对象
const book: Book = {
  title: 'Javascript',
  author:  'Brendan Eich',
  numOfPages: 1024
}

// 尝试改变属性
book.title = 'Typescript'
book.author = 'Anders Hejlsberg'
book.numOfPages = 2048

// 打印 book的值:
console.log(book)

// Output:
// {
//   "title": "Typescript",
//   "author": "Anders Hejlsberg",
//   "numOfPages": 2048
// }

// 将 Book 的所有属性设置为只读:
const book: Readonly<Book> = {
  title: 'Javascript',
  author: ' Brendan Eich',
  numOfPages: 1024
}
// 尝试改变属性
sevenPowers.title = 'Typescript'
// TS error:  Cannot assign to 'title' because it is a read-only property.

Record

假设我们有一组属性名和属性值。也就是我们常常在 Javascript 中使用的 {key: value}。基于此数据,Record<Keys, Type> 允许我们创建键值对的记录。Record<Keys, Type> 通过将 keys 参数指定的所有属性类型与 Type 参数指定的值类型进行映射,基本上创建了一个新接口。

// 创建Table类型
type Table = Record<'width' | 'height' | 'length', number>;
// type Table is basically ('width' | 'height' | 'length' are keys, number is a value):
// interface Table {
//   width: number;
//   height: number;
//   length: number;
// }

// 根据Table类型创建新对象:
const smallTable: Table = {
  width: 50,
  height: 40,
  length: 30
}

// 根据Table类型创建新对象:
const mediumTable: Table = {
  width: 90,
  length: 80
}
// TS error: Property 'height' is missing in type '{ width: number; length: number; }' but required in type 'Table'.

// 创建类型与一些字符串键:
type PersonKeys = 'firstName' | 'lastName' | 'hairColor'

// 创建一个使用 Personkeys 类型:
type Person = Record<PersonKeys, string>
// type Person is basically (personKeys are keys, string is a value):
// interface Person {
//   firstName: string;
//   lastName: string;
//   hairColor: string;
// }

const jane: Person = {
    firstName: 'Jane',
    lastName: 'Doe',
    hairColor: 'brown'
}

const james: Person = {
    firstName: 'James',
    lastName: 'Doe',
}
// TS error: Property 'hairColor' is missing in type '{ firstName: string; lastName: string; }' but required in type 'Person'.

type Titles = 'Javascript' | 'Typescript' | 'Python'

interface Book {
  title: string;
  author: string;
}

const books: Record<Titles, Book> = {
  Javascript: {
    title: 'Javascript',
    author: 'Brendan Eich'
  },
  Typescript: {
    title: 'Typescript',
    author: 'Anders Hejlsberg'
  },
  Python: {
    title: 'Python',
    author: 'Guido Van Rossum'
  },
}

// Record<Titles, Book> 基本上相当于:
Javascript: { // <= "Javascript" 键是指定的 "Titles".
  title: string,
  author: string,
}, // <= "Javascript" 值是指定的 "Book".
Typescript: { // <= "Typescript" 键是指定的 "Titles".
  title: string,
  author: string,
}, // <= "Typescript" 值是指定的 "Book".
Python: { // <= "Python" 键是指定的 "Titles".
  title: string,
  author: string,
} // <= "Python" 值是指定的 "Book".

Pick

假设我们只想使用现有接口的一些属性。可以做的一件事是创建新接口,只使用这些属性。另一个选项是使用 Pick<Type, Keys>Pick 类型允许我们获取现有类型(type),并从中只选择一些特定的键(keys),而忽略其余的。这个类型和 lodash.pick 工具函数功能一样,如果你理解这个函数,那么对于这个类型理解就很轻松了。

// 创建一个 Beverage  接口:
interface Beverage {
  name: string;
  taste: string;
  color: string;
  temperature: number;
  additives: string[] | [];
}

// 创建一个仅使用“name”,“taste”和“color”属性 Beverage 类型:
type SimpleBeverage = Pick<Beverage, 'name' | 'taste' | 'color'>;

// 把 Basically  转化成:
// interface SimpleBeverage {
//   name: string;
//   taste: string;
//   color: string;
// }

// 使用 SimpleBeverage 类型创建新对象:
const water: SimpleBeverage = {
  name: 'Water',
  taste: 'bland',
  color: 'transparent',
}

Omit

Omit<Type, Keys> 基本上是一个相反的 Pick<Type, Keys>。我们指定某些类型作为 type 的参数,但不是选择我们想要的属性,而是选择希望从现有类型中省略的属性。这个类型和 lodash.omit 工具函数功能一样,如果你理解这个函数,那么对于这个类型理解就很轻松了。

// 创建一个 Car 接口:
interface Car {
  model: string;
  bodyType: string;
  numOfWheels: number;
  numOfSeats: number;
  color: string;
}

// 基于 Car 接口创建 Boat 类型,但省略 “numOfWheels” 和 “bodyType” 属性
type Boat = Omit<Car, 'numOfWheels' | 'bodyType'>;

// 把 Boat 转化成:
// interface Boat {
//   model: string;
//   numOfSeats: number;
//   color: string;
// }

// 基于 Car 创建新对象:
const tesla: Car = {
  model: 'S',
  bodyType: 'sedan',
  numOfWheels: 4,
  numOfSeats: 5,
  color: 'grey',
}

// 基于 Boat 创建新对象:
const mosaic: Boat = {
  model: 'Mosaic',
  numOfSeats: 6,
  color: 'white'
}

Exclude

初次使用 Exclude<Type, ExcludedUnion> 可能有点令人困惑。这个实用工具类型所做的是,用于从类型 Type 中取出不在 ExcludedUnion 类型中的成员。

// 创建 Colors 类型:
type Colors = 'white' | 'blue' | 'black' | 'red' | 'orange' | 'grey' | 'purple';

type ColorsWarm = Exclude<Colors, 'white' | 'blue' | 'black' | 'grey'>;
// 把 ColorsWarm 转化成:
// type ColorsWarm = "red" | "orange" | "purple";

type ColorsCold = Exclude<Colors, 'red' | 'orange' | 'purple'>;
// 把 ColorsCold 转化成:
// type ColorsCold = "white" | "blue" | "black" | "grey"

// 创建 varmColor:
const varmColor: ColorsWarm = 'red'

// 创建 coldColor:
const coldColor: ColorsCold = 'blue'

// 尝试混合:
const coldColorTwp: ColorsCold = 'red'
// TS error: Type '"red"' is not assignable to type 'ColorsCold'.

Extract

Extract<Type, Union> 类型执行与 Exclude<Type, ExcludedUnion> 类型相反的操作。用于从类型 Type 中取出可分配给 Union 类型的成员。有点类似集合里的交集概念。使用 Extract 之后,返回 TypeUnion 交集。

type Food = 'banana' | 'pear' | 'spinach' | 'apple' | 'lettuce' | 'broccoli' | 'avocado';

type Fruit= Extract<Food, 'banana' | 'pear' | 'apple'>;
// 把 Fruit 转换成:
// type Fruit = "banana" | "pear" | "apple";

type Vegetable = Extract<Food, 'spinach' | 'lettuce' | 'broccoli' | 'avocado'>;
// 把 Vegetable 转换成:
// type Vegetable = "spinach" | "lettuce" | "broccoli" | "avocado";

// 创建 someFruit:
const someFruit: Fruit = 'pear'

// 创建 someVegetable:
const someVegetable: Vegetable = 'lettuce'

// 尝试混合:
const notReallyAFruit: Fruit = 'avocado'
// TS error: Type '"avocado"' is not assignable to type 'Fruit'.

NonNullable

NonNullable 实用工具类型的工作原理与 Exclude 类似。它接受指定的某种类型,并返回该类型,但不包括所有 nullundefined 类型。

// 创建类型:
type prop = string | number | string[] | number[] | null | undefined;

// 基于以前的类型创建新类型,不包括 null 和 undefined:
type validProp = NonNullable<prop>
// 把 validProp 转换成:
// type validProp = string | number | string[] | number[]

// 这是有效的:
let use1: validProp = 'Jack'

let use2: validProp = null
// TS error: Type 'null' is not assignable to type 'validProp'.

Parameters

Parameters 类型返回一个 Tuple 类型,其中包含作为 Type 传递的形参函数的类型。这些参数的返回顺序与它们在函数中出现的顺序相同。注意,Type 参数,对于 this 和以下类型,是一个函数 ((…args) =>type),而不是一个类型,比如 string

// 声明函数类型:
declare function myFunc(num1: number, num2: number): number;

// 使用 Parameter<type> 从 myFunc 函数的参数创建新的 Tuple 类型:
type myFuncParams = Parameters<typeof myFunc>;
// 把 myFuncParams 转换成:
// type myFuncParams = [num1: number, num2: number];

//  这是有效的:
let someNumbers: myFuncParams = [13, 15];

let someMix: myFuncParams = [9, 'Hello'];
// TS error: Type 'string' is not assignable to type 'number'.

// 使用 Parameter<type> 从函数的参数创建新的 Tuple 类型:
type StringOnlyParams = Parameters<(foo: string, fizz: string) => void>;
// 把 StringOnlyParams 转换成:
// type StringOnlyParams = [foo: string, fizz: string];

//  这是有效的:
let validNamesParams: StringOnlyParams = ['Jill', 'Sandy'];

let invalidNamesParams: StringOnlyParams = [false, true];
// TS error: Type 'boolean' is not assignable to type 'string'.

ConstructorParameters

ConstructorParameters 类型与 Parameters 类型非常相似。这两者之间的区别在于, Parameters 从函数参数中获取类型,而ConstructorParameters 从作为 Type 参数传递的构造函数(Constructor)中获取类型。

// 创建一个 class:
class Human {
  public name
  public age
  public gender

  constructor(name: string, age: number, gender: string) {
    this.name = name;
    this.age = age;
    this.gender = gender;
  }
}

// 创建基于 Human 构造函数类型:
type HumanTypes = ConstructorParameters<typeof Human>
// 把 HumanTypes 转换成:
// type HumanTypes = [name: string, age: number, gender: string]

const joe: HumanTypes = ['Joe', 33, 'male']
const sandra: HumanTypes = ['Sandra', 41, 'female']
const thomas: HumanTypes = ['Thomas', 51]
// TS error: Type '[string, number]' is not assignable to type '[name: string, age: number, gender: string]'.
// Source has 2 element(s) but target requires 3.

// 创建基于 String 构造函数类型:
type StringType = ConstructorParameters<StringConstructor>
// 把 StringType 转换成:
// type StringType = [value?: any]

ReturnType

ReturnType 也类似于 Parameters 类型。这里的不同之处在于,ReturnType 提取作为 type 参数传递的函数的返回类型。

// 声明函数类型:
declare function myFunc(name: string): string;

// 使用 ReturnType<Type> 从 myFunc 类型中提取返回类型:
type MyFuncReturnType = ReturnType<typeof myFunc>;
// 把 MyFuncReturnType 转换成:
// type MyFuncReturnType = string;

// 这是有效的:
let name1: MyFuncReturnType = 'Types';

// 这是有效的:
let name2: MyFuncReturnType = 42;
// TS error: Type 'number' is not assignable to type 'string'.

type MyReturnTypeBoolean = ReturnType<() => boolean>
// 把 MyReturnTypeBoolean 转换成:
// type MyReturnTypeBoolean = boolean;

type MyReturnTypeStringArr = ReturnType<(num: number) => string[]>;
// 把 MyReturnTypeStringArr 转换成:
// type MyReturnTypeStringArr = string[];

type MyReturnTypeVoid = ReturnType<(num: number, word: string) => void>;
// 把 MyReturnTypeVoid 转换成:
// type MyReturnTypeVoid = void;

InstanceType

Instancetype 有点复杂。它所做的就是从作为 Type 参数传递的构造函数的实例类型创建一个新类型。如果使用一个常规类来处理类,则可能不需要此实用工具类型。可以只使用类名来获取所需的实例类型。

// 创建一个 class:
class Dog {
  name = 'Sam'
  age = 1
}

type DogInstanceType = InstanceType<typeof Dog>
// 把 DogInstanceType 转换成:
// type DogInstanceType = Dog

// 类似于使用 class 声明:
type DogType = Dog
// 把 DogType 转换成:
// type DogType = Dog

ThisParameterType

ThisParameterType 提取了作为 Type 参数传递的函数的 this 形参的使用类型。如果函数没有这个参数,实用工具类型将返回unknown

// 创建一个使用 this 参数函数:
function capitalize(this: String) {
  return this[0].toUpperCase + this.substring(1).toLowerCase()
}

// 创建基于 this 参数的 capitalize函数类型:
type CapitalizeStringType = ThisParameterType<typeof capitalize>
// 把 CapitalizeStringType 转换成:
// type CapitalizeStringType = String

// 创建一个不使用 this 参数函数:
function sayHi(name: string) {
  return `Hello, ${name}.`
}

// 创建基于不带 this 参数的 printUnknown 函数类型:
type SayHiType = ThisParameterType<typeof sayHi>
// 把 SayHiType 转换成:
// type SayHiType = unknown

OmitThisParameter

OmitThisParameter 实用类型执行与前面类型相反的操作。它通过 Type 接受一个函数类型作为参数,并返回不带 this 形参的函数类型。

// 创建一个使用 this 参数函数:
function capitalize(this: String) {
  return this[0].toUpperCase + this.substring(1).toLowerCase()
}

// 根据 capitalize 函数创建类型:
type CapitalizeType = OmitThisParameter<typeof capitalize>
// 把 CapitalizeStringType 转换成:
// type CapitalizeType = () => string

// 创建一个不使用 this 参数函数:
function sayHi(name: string) {
  return `Hello, ${name}.`
}

// 根据 Sayhi 函数创建类型:
type SayHiType = OmitThisParameter<typeof sayHi>
// 把 SayHiType 转换成:
// type SayHiType = (name: string) => string

ThisType

ThisType 实用工具类型允许显式地设置 this 上下文。可以使用它为整个对象字面量或仅为单个函数设置此值。在尝试此操作之前,请确保启用了编译器标志 --noImplicitThis

// 创建 User 对象接口:
interface User {
    username: string;
    email: string;
    isActivated: boolean;
    printUserName(): string;
    printEmail(): string;
    printStatus(): boolean;
}

// 创建用户对象,并将 ThisType 设置为 user interface:
const userObj: ThisType<User> = {
  username: 'Jiayi',
  email: 'jiayi@qq.com',
  isActivated: false,
  printUserName() {
    return this.username;
  },
  printEmail() {
    return this.email;
  },
  printStatus() {
    return this.isActivated;
  }
}

联合工具类型

TypeScript 内置实用工具类型的一个好处是,我们可以自由组合它们。可以将一种实用工具类型与另一种实用工具类型组合。还可以将一种实用工具类型与其他类型组合。例如,可以将类型与联合或交集类型组合。

// 创建 User 接口:
interface User {
  username: string;
  password: string;
}

// 创建 SuperUser 接口:
interface SuperUser {
  clearanceLevel: string;
  accesses: string[];
}

// 结合 User 和 SuperUser 创建 RegularUser 类型
// 让 User 属性必需的和  SuperUser 属性可选的:
type RegularUser = Required<User> & Partial<SuperUser>

// 这是有效的:
const jack: RegularUser = {
  username: 'Jack',
  password: 'some_secret_password_unlike_123456',
}

// 这是有效的:
const jason: RegularUser = {
  username: 'Jason',
  password: 'foo_bar_usually-doesnt_work-that_well',
  clearanceLevel: 'A'
}

// 这将抛出异常:
const jim: RegularUser = {
  username: 'Jim'
}
// TS error: Type '{ username: string; }' is not assignable to type 'RegularUser'.
// Property 'password' is missing in type '{ username: string; }' but required in type 'Required<User>'

扩展内置实用工具类型

虽然上面的内置实用工具类型令人惊叹,但它们并没有涵盖所有的用例,这就是提供更多实用工具的库填补空白的地方。此类库的一个很好的例子是 type-fest,它提供了更多的实用程序。

说在最后

在本文中,我们学习了 Typescript 实用工具类型,以及它们如何帮助我们从现有的类型中自动创建类型,而不会导致重复,从而无需保持相关类型的同步。我们列举一些内置的实用工具类型,我认为它们在我作为开发人员的日常工作中特别有用。在此基础上,我们推荐了 type-fest,这是一个包含许多扩展内置类型的实用程序类型的库。

今天就到这里吧,伙计们,玩得开心,祝你好运。