TypeScript入门之泛型

简介: 泛型(generic)是 TypeScript中非常重要的一个概念,因为在之后实际开发中任何时候都离不开泛型的帮助,原因就在于泛型给予开发者创造灵活、可重用代码的能力。

单个类型参数

假设我们用一个函数,它可接受一个 number 参数并返回一个 number 参数。

function returnItem (param: number): number {
  return param
}

我们按以上的写法貌似是没问题的,那么如果我们要接受一个 string 并返回同样一个 string 呢?逻辑是一样的,但是仅仅是类型发生了变化,难道需要再写一遍?

function returnItem (param: string): string {
  return param
}

这明显是重复性的代码,我们应该如何才能避免上述情况呢?

难道我们只能用 any 表示了?

function returnItem (param: any): any {
  return param
}

我们现在的情况是,我们在静态编写的时候并不确定传入的参数到底是什么类型,只有当在运行时传入参数后我们才能确定。

那么我们需要变量,这个变量代表了传入的类型,然后再返回这个变量,它是一种特殊的变量,只用于表示类型而不是值。

这个类型变量在 TypeScript 中就叫做「泛型」。

function returnItem<T>(param: T): T {
  return param
}

我们在函数名称后面声明泛型变量 <T>,它用于捕获开发者传入的参数类型(比如说string),然后我们就可以使用T(也就是string)做参数类型和返回值类型了。

console.log(returnItem<string>("randy"));
console.log(returnItem<number>(1));

多个类型参数

定义泛型的时候,可以一次定义多个类型参数,多个类型参数用逗号分开。

比如我们可以同时定义泛型 T 和 泛型 U

function swap<T, U>(tuple: [T, U]): [U, T] {
  return [tuple[1], tuple[0]];
}

console.log(swap<number, string>([27, "randy"])); // ['randy', 27]
注意:通常使用单个字母来命名泛型类型。这不是语法规则,我们也可以像 TypeScript 中的任何其他类型一样命名泛型,但这种约定有助于向阅读代码的人传达泛型类型不需要特定类型。

默认类型

定义泛型方法后,如果是简单方法,我们可以不用显示传递泛型类型TS的类型推断会帮我们推算出来。但是复杂情况下每次都要我们显示传递泛型类型。

type Gen1 = {
  name: string
}

async function genfun1() {
  async function fetchApi<T>(path: string): Promise<T> {
    const response = await fetch(path);
    return response.json();
  }
  
  const result = await fetchApi<Gen1>('/test')
  

  console.log(result.name) // OK
}

如果很多方法的返回值都是一样的,我们不想每次使用方法的时候都去传递一遍泛型类型该怎么办呢?

这就可以用到默认类型啦。使用形式跟默认参数一样,使用=即可。

type Gen1 = {
  name: string
}

async function genfun1() {
  async function fetchApi<T = Gen1>(path: string): Promise<T> {
    const response = await fetch(path);
    return response.json();
  }
  
  const result = await fetchApi('/test')
  
  console.log(result.name) // OK
}

泛型约束

现在有一个问题,我们的泛型现在似乎可以是任何类型,但是我们明明知道我们的传入的泛型属于哪一类,比如属于 number 或者 string 其中之一,那么应该如何约束泛型呢?

class Stack<T> {
  private arr: T[] = []

  public push(item: T) {
    this.arr.push(item)
  }

  public pop() {
    this.arr.pop()
  }
}

我们可以用 <T extends xx> 的方式约束泛型,比如下图显示我们约束泛型为 number 或者 string 之一,当传入 boolean 类型的时候,就会报错。

type Union1 = string | number

class Stack2<T extends Union1> {
  private arr: T[] = []

  public push(item: T) {
    this.arr.push(item)
  }

  public pop() {
    this.arr.pop()
  }
}

const stack2 = new Stack2<string>()
const stack3 = new Stack2<number>()
const stack4 = new Stack2<boolean>() // Error

image.png

泛型接口和泛型类

泛型接口

interface Inter1<T> {
  param: T
}

const param1: Inter4<string> = {param: 123} // Error 不能将类型“number”分配给类型“string”。
const param2: Inter4<string> = {param: 'randy'} // OK

我们知道接口也是可以定义函数的,以上面的函数为例,如果我们将其转化为接口的形式。

interface ReturnItemFn<T> {
  (param: T): T
}

那么当我们想传入一个number作为参数的时候,就可以这样声明函数:

const returnItem: ReturnItemFn<number> = param => param

那么当我们想传入一个string作为参数的时候,就可以这样声明函数:

const returnItem: ReturnItemFn<string> = param => param

其他类型都可以传递,是不是灵活性大大提升啦。

泛型类

泛型除了可以在函数中使用,还可以在类中使用,它既可以作用于类本身,也可以作用与类的成员函数。

我们假设要写一个数据结构,它的简化版是这样的:

class Stack {
  private arr: number[] = []

  public push(item: number) {
    this.arr.push(item)
  }

  public pop() {
    this.arr.pop()
  }
}

同样的问题,如果只是传入 number 类型就算了,可是需要不同的类型的时候,还得靠泛型的帮助。

class Stack<T> {
  private arr: T[] = []

  public push(item: T) {
    this.arr.push(item)
  }

  public pop() {
    this.arr.pop()
  }
}

我们在实例化类的时候就可以传递各种类型啦。

const stack1 = new Stack<number>()
stack1.push(1)

const stack2 = new Stack<string>()
stack2.push('randy')

泛型类看上去与泛型接口差不多, 泛型类使用 <> 括起泛型类型,跟在类名后面。

索引类型

我们先看一个场景,现在我们需要一个 pick 函数,这个函数可以从对象上取出指定的属性,是的,就是类似于 lodash.pick 的方法。

在 JavaScript 中这个函数应该是这样的:

function pick(o, names) {
  return names.map(n => o[n]);
}

如果我们从一个 user 对象中取出 id ,那么应该这样:

const user = {
  username: 'randy',
  id: 2300002033333333,
  token: '230000201922222',
  avatar: 'http://randy.jpg',
  role: 'admin'
}
const res = pick(user, ['id'])

console.log(res) // [ '4600002033333333' ]

那么好了,我们应该如何在 TypeScript 中实现上述函数?结合我们之前学到的知识,你会怎么做?

如何描述 pick 函数的第一个参数 o 呢?你可能会想到之前提到过的可索引类型,这个对象的 key 都是 string 而对应的值可能是任意类型,那么可以这样表示:

interface Obj {
  [key: string]: any
}

而第二个参数 names 很明显是个字符串数组,这个函数其实很容易就用 TypeScript 写出来了:

function pick(o: Obj, names: string[]) {
  return names.map(n => o[n]);
}

这样似乎没什么问题,但是如果你够细心的话,还是会发现我们的类型定义不够严谨:

  • 参数 names 的成员应该是参数 o 的属性,因此不应该是 string 这种宽泛的定义,应该更加准确
  • 我们 pick 函数的返回值类型为 any[],其实可以更加精准,pick 的返回值类型应该是所取的属性值类型的联合类型

我们应该如何更精准的定义类型呢?

这里我们必须了解两个类型操作符:索引类型查询操作符索引访问操作符

索引类型查询操作符

索引类型查询操作符使用keyof关键字。我们可以用 keyof 作用于泛型 T 上来获取泛型 T 上的所有 public 属性名构成联合类型。

class User6 {
  public name: string;
  public age: number;

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

// 对象键属性
type objKeys = keyof User6; // name | age

keyof 正是赋予了开发者查询索引类型的能力。

索引访问操作符

我们可以通过 keyof 查询索引类型的属性名,那么如何获取属性名对应的属性值类型呢?因为在上面提到的 pick 函数中,我们确实有一个需求时获取属性名对应的属性值类型的需求。

这就需要索引访问符出场了,与 JavaScript 种访问属性值的操作类似,访问类型的操作符也是通过 [] 来访问的,即 T[K]

class User6 {
  public name: string;
  public age: number;

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

// 对象键属性
type objKeys = keyof User6; // name | age
// 对象值类型
type objValues = User6[objKeys] // string | number

当我们了解了这两个访问符之后,上面的问题就迎刃而解了。

首先我们需要一个泛型 T 它来代表传入的参数 o 的类型,因为我们在编写代码时无法确定参数 o 的类型到底是什么,所以在这种情况下要获取 o 的类型必须用面向未来的类型--泛型。

那么传入的第二个参数 names ,它的特点就是数组的成员必须由参数 o 的属性名称构成,这个时候我们很容易想到刚学习的操作符keyofkeyof T代表参数o类型的属性名的联合类型,我们的参数names的成员类型K则只需要约束到keyof T即可。

我们的返回值就更简单了,我们通过类型访问符T[K]便可以取得对应属性值的类型,他们的数组T[K][]正是返回值的类型。

function pick<T, K extends keyof T>(o: T, names: K[]): T[K][] {
    return names.map(n => o[n]);
}

const res = pick(user, ['token', 'id', ])

我们用索引类型结合类型操作符完成了 TypeScript 版的 pick 函数,它不仅仅有更严谨的类型约束能力,也提供了更强大的代码提示能力:

image.png

映射类型

在了解映射类型之前,我们不妨看一个例子.

我们有一个User接口,现在有一个需求是把User接口中的成员全部变成可选的,我们应该怎么做?难道要重新一个个:前面加上?,有没有更便捷的方法?

interface User {
  username: string
  id: number
  token: string
  avatar: string
  role: string
}

这个时候映射类型就派上用场了,映射类型的语法是[K in Keys]:

  • K:类型变量,依次绑定到每个属性上,对应每个属性名的类型
  • Keys:字符串字面量构成的联合类型,表示一组属性名(的类型)

那么我们应该如何操作呢?

首先,我们得找到Keys,即字符串字面量构成的联合类型,这就得使用上一节我们提到的keyof操作符,假设我们传入的类型是泛型T,得到keyof T,即传入类型T的属性名的联合类型。

然后我们需要将keyof T的属性名称一一映射出来[K in keyof T],如果我们要把所有的属性成员变为可选类型,那么需要T[K]取出相应的属性值,最后我们重新生成一个可选的新类型{ [K in keyof T]?: T[K] }

用类型别名表示就是:

type partial<T> = { [K in keyof T]?: T[K] }

我们做下测试

type partialUser = partial<User>

果然所有的属性都变成了可选类型:

image.png

系列文章

TypeScript入门之环境搭建

TypeScript入门之数据类型

TypeScript入门之函数

TypeScript入门之接口

TypeScript入门之类

TypeScript入门之类型推断、类型断言、双重断言、非空断言、确定赋值断言、类型守卫、类型别名

TypeScript入门之泛型

TypeScript入门之装饰器

TypeScript入门之模块与命名空间

TypeScript入门之申明文件

TypeScript入门之常用内置工具类型

TypeScript入门之配置文件

后记

感谢小伙伴们的耐心观看,本文为笔者个人学习笔记,如有谬误,还请告知,万分感谢!如果本文对你有所帮助,还请点个关注点个赞~,您的支持是笔者不断更新的动力!

相关文章
|
4月前
|
JavaScript 编译器
typescript之泛型
typescript之泛型
137 60
|
3月前
|
JavaScript 前端开发
TypeScript【类型别名、泛型】超简洁教程!再也不用看臭又长的TypeScript文档了!
【10月更文挑战第11天】TypeScript【类型别名、泛型】超简洁教程!再也不用看臭又长的TypeScript文档了!
|
4月前
|
JavaScript 安全
typeScript进阶(14)_泛型和注意事项
TypeScript中的泛型允许创建可重用的代码。泛型可以定义函数、接口、类,支持传递类型参数,实现类型安全。泛型可以用于数组,约束类型参数必须符合特定的接口,也可以在接口和类中使用。泛型类可以包含多个类型参数,甚至在泛型约束中使用类型参数。
30 1
typeScript进阶(14)_泛型和注意事项
|
3月前
|
JavaScript 前端开发 编译器
【小白入门】 浏览器如何识别Typescript?
【10月更文挑战第1天】浏览器如何识别Typescript?
|
3月前
|
JavaScript 安全 前端开发
TypeScript :枚举&字符&泛型
本文介绍了 TypeScript 中的泛型、约束、枚举和字符操作的基本用法。通过示例代码展示了如何定义和使用泛型函数、类和接口,以及如何利用 `keyof` 约束类型。此外,还介绍了枚举的定义和使用,包括常量枚举和外部枚举的区别。最后,简要说明了 `?.` 和 `??` 操作符的用途,帮助处理可能为空的属性和提供默认值。
|
4月前
|
JavaScript 前端开发 编译器
TypeScript,从0到入门带你进入类型的世界
该文章提供了TypeScript的入门指南,从安装配置到基础语法,再到高级特性如泛型、接口等的使用,帮助初学者快速掌握TypeScript的基本用法。
|
5月前
|
JavaScript 安全 算法
TypeScript:一个好泛型的价值
TypeScript:一个好泛型的价值
|
6月前
|
JavaScript 前端开发 程序员
Typescript 【实用教程】(2024最新版)含类型声明,类型断言,函数,接口,泛型等
Typescript 【实用教程】(2024最新版)含类型声明,类型断言,函数,接口,泛型等
95 0
|
7月前
|
JavaScript
TypeScript 泛型类型
TypeScript 泛型类型
|
7月前
|
JavaScript Java 编译器
TypeScript 泛型
TypeScript 泛型