乐趣区

关于typescript:TypeScript-进阶与实践二

内容接《TypeScript 进阶与实际(一)》,倡议在学习完上一篇内容后再持续浏览这一篇。

三、类型编程

3.1 泛型

泛型是一种创立可复用代码组件的工具。泛型容许咱们在强类型程序设计语言中编写代码时应用一些当前才指定的类型,在应用时作为参数指明这些类型。泛型能够用于定义函数、接口或类,不事后定义好具体的类型,而在应用的时候再指定类型。泛型能够进步代码的可读性和可维护性,同时也能够进步代码的复用性和灵活性。

这部分以函数中如何应用泛型来开展解说,你还能够在类型别名、类和对象等场景应用泛型,应用形式大同小异,这里就不再赘述。

3.1.1 泛型类型变量

假如要定义一个 reflect 函数,它能够接管任意类型的参数,并一成不变地返回参数的值,在没有应用泛型之前,咱们能够这样去实现:

function reflect(param: unknown) {return param;}

const str = reflect('string'); // str 类型为 unknown
const num = reflect(1); // num 类型为 unknown

你会发现,这种实现形式,不论咱们传入的参数是什么类型,最终返回的值的类型都是 unknown,这样在应用返回值时还须要将类型放大能力平安的应用,并不是很不便。咱们曾经晓得 reflect 函数会一成不变的返回传入的参数了,那么有没有方法让返回值的类型依据传入的参数的类型变动而变动呢?答案是必定的,应用泛型类型变量就能够满足这个要求。

泛型类型变量的应用形式如上面示例所示:

function reflect<T>(param: T) {return param;}

// TS 会依据参数的值的类型来推断泛型类型参数的类型
let str = reflect('string'); // str 的类型为 string
let num = reflect(1); // num 的类型为 num

// 显式传递泛型类型参数,会限度传入的参数类型必须与传入的泛型参数类型统一
let value = reflect<string>(123); // str 的类型为 string

下面示例中咱们在 reflect 函数名称的前面应用 <T> 申明了泛型类型变量 T,而后将参数 param 的类型指定为了 T,函数体中一成不变的返回了参数 param,返回值的类型会被隐式推断为 param 的类型,即返回值的类型也为 T

3.1.2 多个泛型类型参数

要定义多个泛型类型参数时,只须要用半角逗号将多个泛型类型参数名隔开即可,如上面这个示例所示:

function genericTypeVariables<T, P>(a: T, b: P) {return { a, b};
}

3.1.3 泛型类型参数束缚

泛型类型参数束缚能够限度传入的泛型类型参数的类型,它通过 extends 关键字来实现。应用泛型类型参数束缚,能够确保泛型类型参数满足特定的条件,从而进步代码的可读性和可维护性。如上面的示例所示:

function getName<T extends {name: string}>(obj: T) {return obj.name;}

let result1 = getName({name: 'Jason'}); // result1 类型为 string
let result2 = getName({age: 18}); // @error: 2345

在这个示例中咱们限度了泛型类型参数 T 的类型必须蕴含属性 name 且属性 name 的类型必须为 string。在调用该泛型函数时,如果传入的参数的类型不蕴含 name 属性就会产生谬误。

3.1.4 泛型默认值

泛型类型参数还反对设置默认值,以下是一个 TypeScript 泛型参数带默认值的示例:

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

function identity<O extends Person, R = string>(obj: O, format?: (obj: O) => R): R {return format ? format(obj) : (obj.name as R);
}

// result1 的类型为泛型类型参数 R 的默认类型 string
const result1 = identity({name: 'John', age: 30});

// result2 的类型为 number
const result2 = identity({name: 'John', age: 30}, obj => obj.age);

在这个示例中,泛型类型参数 R 带有默认值 string。如果没有显式地给出类型参数,那么 R 将被推断为 string 类型。在不指定类型参数的状况下调用函数 identity,且不传入 format 参数时,返回值的类型为泛型类型参数 R 的默认类型 string,当传入 format 参数,TypeScript 依据传入的参数的类型推断出泛型类型参数 R 的类型为 number,因而 result2 的类型为 number

3.2 keyof 类型运算符

keyof 类型运算符 用于从对象类型中提取键类型。例如,如果咱们有一个对象类型:

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

咱们能够应用 keyof 来提取它的键类型:

type PersonKeys = keyof Person; // "name" | "age" | "address"

这将返回一个联结类型,蕴含对象的所有键。这个联结类型能够用于确定对象是否具备某个属性,如上面示例所示:

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

function getProperty<T, K extends keyof T>(obj: T, key: K) {return obj[key];
}

const person: Person = {
  name: 'John',
  age: 30,
  location: 'Seattle',
};

console.log(getProperty(person, 'name')); // John
console.log(getProperty(person, 'age')); // 30
console.log(getProperty(person, 'location')); // Seattle

// @error: Argument of type '"name1"' is not assignable to parameter of type 'keyof Person'.(2345)
console.log(getProperty(person, 'name1'));

3.3 typeof 类型运算符

typeof 类型运算符 用于获取变量或对象的类型。如上面示例所示:

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

const person: Person = {
  name: 'John',
  age: 30,
  address: 'Seattle',
};

type PersonType = typeof person; // {name: string; age: number; address: string;}

当咱们导入一个函数,但该函数没有提供其参数和返回值的类型时,咱们能够应用 TypeScript 中的 typeof 关键字来提取函数的类型。这是一个简略的示例,演示了如何应用 typeof 关键字来提取函数参数的类型:

import {myFunction} from './myModule';

// Parameters 工具类型能够从一个函数类型中提取函数的参数类型,将在后续章节讲到
type MyFunctionArgs = Parameters<typeof myFunction>;

let args: MyFunctionArgs = ...;
myFunction(...args);

在这个示例中,咱们首先从 myModule 模块中导入了一个名为 myFunction 的函数。而后,咱们应用 typeof 关键字来获取 myFunction 函数的类型,并应用 Parameters 工具类型来提取该函数的参数类型。最初,咱们定义了一个名为 args 的变量,其类型为 MyFunctionArgs,并将其作为 myFunction 函数的参数传递。

3.4 索引拜访类型

索引拜访类型 索引拜访类型是一种类型操作符,用于获取对象或类型的属性类型。例如,如果咱们有一个对象类型:

interface Person {
  name: string;
  age: number;
  alive: boolean;
}

type Age = Person['age'] 将返回一个类型,即 Person 对象的 age 属性的类型。咱们还能够应用联结类型、keyof 或其余形式来应用索引拜访类型。例如:

type I1 = Person['age' | 'name']; // string | number
type I2 = Person[keyof Person]; // string | number | boolean
type AliveOrName = 'alive' | 'name';
type I3 = Person[AliveOrName]; // string | boolean

3.5 条件类型

条件类型是 TypeScript 2.8 版本中引入的一种类型。它能够帮忙咱们依据输出类型形容输出和输入类型之间的关系。条件类型的模式看起来有点像 JavaScript 中的条件表达式 condition ? trueExpression : falseExpressionSomeType extends OtherType ? TrueType : FalseType。当 extends 右边的类型能够调配给左边的类型时,你将取得第一个分支中的类型(TrueType);否则,您将取得后一个分支中的类型(FalseType

这是一个 TypeScript 中条件类型的示例:

type IsNumber<T> = T extends number ? true : false;
const isNumber: IsNumber<42> = true; // isNumber is of type "true"

在这个例子中,咱们定义了一个名为 IsNumber 的条件类型,它查看类型 T 是否为 number 类型。如果是,则返回 true 类型,否则返回 false 类型。而后,咱们应用这个条件类型来定义一个常量 isNumber,并将其类型设置为 IsNumber<42>。因为 42 是一个数字,所以 isNumber 的类型为 true

3.2.1 infer

在条件类型中能够应用 infer 关键字 进行类型推断。在 TypeScript 官网应用了 ReturnType 这一经典例子阐明它的作用:

type ReturnType<T> = T extends (...args: any[]) => infer R ? R : never;

下面这个例子中,如果泛型类型变量 T 继承了 extends (...args: any[]) => any 类型,则返回类型 R,否则返回 never。其中类型变量 R 被定义在 extends (...args: any[]) => infer R 中应该标注返回值类型的地位,即 R 是依据传入参数的类型的返回值的类型推导进去的。

3.2.2 调配条件类型

调配条件类型是 TypeScript 中的一种条件类型,它用来表白非平均类型映射。当传入的类型参数为联结类型时,它们会被调配类型。例如,假如咱们有一个名为 ToArray 的类型,它将一个类型转换为数组类型:

type ToArray<Type> = Type extends any ? Type[] : never;

如果咱们将联结类型传入 ToArray,则条件类型将利用于该联结类型的每个成员:

// type StrArrOrNumArr = string[] | number[]
type StrArrOrNumArr = ToArray<string | number>;

这里 string | number 别离与 ToArray 联合产生了 string[]number[] 组成的联结类型。

3.6 映射类型

映射类型可能将一个类型映射成另一个类型。

例如,假如咱们有一个名为 OldType 的类型,它具备三个字符串属性:

type OldType = {a: string; b: string; c: string};

咱们能够应用映射类型将 OldType 中的每个属性映射到具备数字值的新类型 NewType

// 新类型为 {a: number, b: number, c: number}
type NewType = {[P in keyof OldType]: number };

3.7 模板字面量类型

模板字面量类型是 TypeScript 4.1 开始反对的一种类型。它基于字符串字面量类型,能够开展为多个字符串类型的联结类型。其语法与 JavaScript 中的模板字面量是统一的,然而是用在类型的地位上。

例如,当与某个具体的字面量类型一起应用时,模板字面量会将文本连贯从而生成一个新的字符串字面量类型。

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

const str1: Greeting = 'hello world';
const str2: Greeting = 'hello'; // @error: Type '"hello"' is not assignable to type '"hello world"'.(2322)

如果在替换字符串的地位是联结类型,那么后果类型是由每个联结类型成员形成的字符串字面量的汇合:

type EmailLocaleIDs = 'welcome_email' | 'email_heading';
type FooterLocaleIDs = 'footer_title' | 'footer_sendoff';

// "welcome_email_id" | "email_heading_id" | "footer_title_id" | "footer_sendoff_id"
type AllLocaleIDs = `${EmailLocaleIDs | FooterLocaleIDs}_id`;

const str1: AllLocaleIDs = 'welcome_email_id';
const str2: AllLocaleIDs = 'email_heading_id';
const str3: AllLocaleIDs = 'footer_title_id';
const str4: AllLocaleIDs = 'footer_sendoff_id';
const str5: AllLocaleIDs = 'hello'; // @error: Type '"hello"' is not assignable to type '"welcome_email_id" | "email_heading_id" | "footer_title_id" | "footer_sendoff_id"'.(2322)

模板字面量类型能够用于限度办法的字符串入参的格局、对象的属性命名格局等场景,上面是一个示例:

type PrefixKeys<T, K extends string> = {[P in keyof T & string as `${K}${P}`]: T[P] };

type Example = {
  a: number;
  b: string;
};

type PrefixedExample = PrefixKeys<Example, 'x-'>;
// 等同于
// type PrefixedExample = {
//     'x-a': number;
//     'x-b': string;
// }

3.8 内置工具类型

TypeScript 内置了一些根本的工具类型,能够间接应用。这些工具类型都定义在 TypeScript 外围库的定义文件中。这些工具类型能够帮忙开发者更好地利用根底类型,免得反复造轮子,并能通过这些工具类型实现更高级的类型操作。

3.8.1 接口操作类型

Partial<Type>

Partial 工具类型能够将一个类型的所有属性变为可选的,且该工具类型返回的类型是给定类型的所有子集。如上面示例所示:

interface Person {
  name: string;
  age: number;
  weight?: number;
}

type PartialPerson = Partial<Person>;
// 相当于
// interface PartialPerson {
//   name?: string;
//   age?: number;
//   weight?: number;
// }

一个常见的应用场景是通过应用 Partial 来定义一个函数参数,该函数只承受对象中的一部分属性:

interface Todo {
  title: string;
  description: string;
}

function updateTodo(todo: Todo, fieldsToUpdate: Partial<Todo>) {return { ...todo, ...fieldsToUpdate};
}

const todo1 = {
  title: 'organize desk',
  description: 'clear clutter',
};

const todo2 = updateTodo(todo1, { description: 'throw out trash'});

Partial 只会将对象类型的间接子属性设置为可选,如果心愿将对象的类型的所有后辈属性都设为可选的,你能够实现一个 DeepPartial 工具类型,代码如下:

type DeepPartial<T extends Record<string, any>> = {[P in keyof T]?: null | DeepPartial<T[P]>;
};
Required<T>

Partial 工具类型相同,Required 工具类型能够将给定类型的所有属性变为必选的,如示例所示:

interface Person {
  name: string;
  age?: number;
  weight?: number;
}

type RequiredPerson = Required<Person>;

// 相当于
// interface RequiredPerson {
//   name: string;
//   age: number;
//   weight: number;
// }
ReadOnly<T>

Readonly 工具类型能够将给定类型的所有属性设为只读,这意味着给定类型的属性不能够被从新赋值,如示例所示:

interface Todo {title: string;}

const todo: Readonly<Todo> = {title: 'Delete inactive users',};

todo.title = 'Hello'; // @errors: 2540

像 React 中组件的状态、属性以及其它一些应用不可变数据类型的场景下,咱们不心愿开发者间接批改对象的属性,就能够应用到这个工具类型。

Pick<T, K>

Pick 工具类型能够从给定的类型当选取出指定的键值,而后组成一个新的类型,如示例所示:

interface Todo {
  title: string;
  description: string;
  completed: boolean;
}

type TodoPreview = Pick<Todo, 'title' | 'completed'>;

const todo: TodoPreview = {
  title: 'Clean room',
  completed: false,
};
Omit<T, K>

Pick 类型相同,Omit 工具类型的性能是返回去除指定的键值之后返回的新类型,如示例所示:

interface Todo {
  title: string;
  description: string;
  completed: boolean;
  createdAt: number;
}

type TodoPreview = Omit<Todo, 'description'>;
// type TodoPreview = {
//   title: string;
//   completed: boolean;
//   createdAt: number;
// }

type TodoInfo = Omit<Todo, 'completed' | 'createdAt'>;
// type TodoInfo = {
//   description: string;
//   title: string;
// }

3.8.2 联结类型相干

Exclude<T, U>

Exclude 的作用是从联结类型中去除指定的类型,这里有点相似 Omit,实际上 Omit 的官网实现就是依赖了 Exclude,如示例所示:

type T0 = Exclude<'a' | 'b' | 'c', 'a'>; // "b" | "c"
type T1 = Exclude<'a' | 'b' | 'c', 'a' | 'b'>; // "c"
type T2 = Exclude<string | number | (() => void), Function>; // string | number

// Omit 实现应用到了 Exclude
type MyOmit<T, K extends keyof any> = {[P in Exclude<keyof T, K>]: T[P];
};
Extract<T, U>

Extract 类型的作用与 Exclude 正好相同,Extract 次要用来从联结类型中提取指定的类型,相似于操作接口类型中的 Pick 类型,如示例所示:

type T0 = Extract<'a' | 'b' | 'c', 'a' | 'f'>; // type T0 = "a"
type T1 = Extract<string | number | (() => void), Function>; // type T1 = () => void
NonNullable<T>

NonNullable 的作用是从联结类型中去除 null 或者 undefined 的类型,如示例所示:

type T0 = Extract<'a' | 'b' | 'c', 'a' | 'f'>; // type T0 = "a"
type T1 = Extract<string | number | (() => void), Function>; // type T1 = () => void
Record<K, T>

Record 的作用是生成接口类型,而后咱们应用传入的泛型参数别离作为接口类型的属性和值。如示例所示:

enum TaskStatus {
  READY = 'ready',
  IN_PROGRESS = 'in_progress',
  DONE = 'done',
}

const mapTaskStatusToColor = {[TaskStatus.READY]: 'yellow',
  [TaskStatus.IN_PROGRESS]: 'green',
};

const mapTaskStatusToName: Record<TaskStatus, string> = {// @error: Property '[TaskStatus.DONE]' is missing...
  [TaskStatus.READY]: '待处理',
  [TaskStatus.IN_PROGRESS]: '解决中',
};

在这个示例中定义了工作状态的枚举值 TaskStatus,而后应用 mapTaskStatusToColor 配置了工作状态和工作显示色彩之间的映射关系,应用 mapTaskStatusToName 配置了工作状态和工作名称之间的映射关系。mapTaskStatusToColor 没有应用 Record 联合来枚举值 TaskStatus 来标注类型,所以在配置时 TaskStatus.DONE 没有配置对应的色彩也不会报错,当工作状态过多的时候你可能因为忽略而没有设置某个工作状态对应的色彩,当你生产 mapTaskStatusToColor 这个对象时你的程序就可能会因为工作状态没有对应的色彩而出 BUG。在 mapTaskStatusToName 应用了 Record<TaskStatus, string> 来限度对象的属性,因而在没有设置 TaskStatus.DONE 的工作状态名称时,TypeScript 会发现对象缺失了这一状态属性,你能够依据错误信息解决这个问题,从而防止了本人因为忽略漏掉。

3.8.3 函数类型相干

ConstructorParameters<T>

ConstructorParameters 能够用来获取构造函数的结构参数,如示例所示:

type T0 = ConstructorParameters<ErrorConstructor>; // type T0 = [message?: string | undefined]
type T1 = ConstructorParameters<FunctionConstructor>; // type T1 = string[]
type T2 = ConstructorParameters<RegExpConstructor>; // type T2 = [pattern: string | RegExp, flags?: string | undefined]
type T3 = ConstructorParameters<any>; // type T3 = unknown[]
type T4 = ConstructorParameters<Function>; // @errors: 2344
Parameters<T>

Parameters 的作用与 ConstructorParameters 相似,Parameters 能够用来获取函数的参数并返回序对,如示例所示:

declare function f1(arg: { a: number; b: string}): void;

type T0 = Parameters<() => string>;
//type T0 = []

type T1 = Parameters<(s: string) => void>;
//type T1 = [s: string]

type T2 = Parameters<<T>(arg: T) => T>;
// type T2 = [arg: unknown]

type T3 = Parameters<typeof f1>;
// type T3 = [arg: {
//     a: number;
//     b: string;
// }]
ReturnType<T>

ReturnType 的作用是用来获取函数的返回类型,如示例所示:

declare function f1(): { a: number; b: string};

type T0 = ReturnType<() => string>;
// type T0 = string

type T1 = ReturnType<(s: string) => void>;
// type T1 = void

type T2 = ReturnType<<T>() => T>;
// type T2 = unknown

type T3 = ReturnType<<T extends U, U extends number[]>() => T>;
// type T3 = number[]

type T4 = ReturnType<typeof f1>;
// type T4 = {
//     a: number;
//     b: string;
// }

3.8.4 更多

更多工具类型能够参考工具类型 – TypeScript Documentation。

四、参考资料

TypeScript 文档

TypeScript Documentation 是 TypeScript 的官网文档站点,官网文档对各个知识点的解说很粗疏并且联合了示例,是学习 TypeScript 最好的形式之一。然而它的官网中文文档翻译的不全面,对英文不是很好的用户不太敌对,中文文档举荐浏览 TypeScript Deep Dive 中文版。

TypeScript 一些在后续版本迭代中减少或者加强的个性,在版本更新日志和官网仓库中能够找到相应的介绍,在官网文档对应的个性介绍局部是没有的,例如 tuple-types – TypeScript: Documentation 中没有介绍命名元组成员(named tuple),而在 TypeScript 4.2 版本更新日志中介绍了元组反对命名并提供了示例:

let d: [first: string, second?: string] = ['hello'];
d = ['hello', 'world'];

因而,在学习 TypeScript 的时候,不要局限于官网文档站,还能够通过以下路径学习:

  • TypeScript 开发博客;
  • TypeScript Github 仓库的 Issue;
  • TypeScript Github 仓库的 Wiki;

这些站点中会提到 TypeScript 设计指标、应用、性能等方面的一些探讨,在官网文档站中是不肯定有的。

TypeScript Playground

TypeScript Playground 是一个在线编辑器,用于摸索 TypeScript 和 JavaScript。它是一个网站,反对设置在线环境的 tsconfig 配置以及应用 TypeScript 的版本,能够让您不便的编写、分享和学习 TypeScript。

Collection of TypeScript type challenges

Collection of TypeScript type challenges 是一个 TypeScript 在线类型编程题目汇合,旨在帮忙你更好地理解类型零碎如何工作,学习编写你本人的工具类型。

如上图所示,你能够点击相应标签查看挑战的详细信息,而后在 TypeScript Playground 中开始挑战,也能够在反对 TypeScript 语言的 IDE 或文本编辑器中开始挑战。

Definitely Typed

Definitely Typed – high quality TypeScript type definitions 是一个我的项目,它提供了一个存储库,用于存储没有类型的 NPM 包的 TypeScript 类型定义。它是由社区保护的,蕴含了许多风行的 JavaScript 库的申明文件。

当你应用这些没有类型的 JavaScript 库时,你能够通过装置对应的 @types 包来取得类型反对。例如,如果你想要在 TypeScript 我的项目中应用 lodash 库,你能够装置 @types/lodash 包来取得类型反对。

npm install --save-dev @types/lodash

装置后,TypeScript 编译器会主动蕴含这些类型定义,你就能够在代码中取得类型提醒和类型查看了。

React + TypeScript Cheat Sheets

React + TypeScript Cheat Sheets 是一个为有教训的 React 开发人员提供 TypeScript 入门指南的备忘单。它侧重于提供通过验证的最佳实际和可复制粘贴的示例,并在过程中解释一些根本的 TypeScript 类型应用和设置。

退出移动版