程序员scholar 程序员scholar
首页
  • Java 基础

    • JavaSE
    • JavaIO
    • JavaAPI速查
  • Java 高级

    • JUC
    • JVM
    • Java新特性
    • 设计模式
  • Web 开发

    • Servlet
    • Java网络编程
  • Web 标准

    • HTML
    • CSS
    • JavaScript
  • 前端框架

    • Vue2
    • Vue3
    • Vue3 + TS
    • 微信小程序
    • uni-app
  • 工具与库

    • jQuery
    • Ajax
    • Axios
    • Webpack
    • Vuex
    • WebSocket
    • 第三方登录
  • 后端与语言扩展

    • ES6
    • Typescript
    • node.js
  • Element-UI
  • Apache ECharts
  • 数据结构
  • HTTP协议
  • HTTPS协议
  • 计算机网络
  • Linux常用命令
  • Windows常用命令
  • SQL数据库

    • MySQL
    • MySQL速查
  • NoSQL数据库

    • Redis
    • ElasticSearch
  • 数据库

    • MyBatis
    • MyBatis-Plus
  • 消息中间件

    • RabbitMQ
  • 服务器

    • Nginx
  • Spring框架

    • Spring6
    • SpringMVC
    • SpringBoot
    • SpringSecurity
  • SpringCould微服务

    • SpringCloud基础
    • 微服务之DDD架构思想
  • 日常必备

    • 开发常用工具包
    • Hutoll工具包
    • IDEA常用配置
    • 开发笔记
    • 日常记录
    • 项目部署
    • 网站导航
    • 产品学习
    • 英语学习
  • 代码管理

    • Maven
    • Git教程
    • Git小乌龟教程
  • 运维工具

    • Docker
    • Jenkins
    • Kubernetes
  • 算法笔记

    • 算法思想
    • 刷题笔记
  • 面试问题常见

    • 十大经典排序算法
    • 面试常见问题集锦
关于
GitHub (opens new window)
首页
  • Java 基础

    • JavaSE
    • JavaIO
    • JavaAPI速查
  • Java 高级

    • JUC
    • JVM
    • Java新特性
    • 设计模式
  • Web 开发

    • Servlet
    • Java网络编程
  • Web 标准

    • HTML
    • CSS
    • JavaScript
  • 前端框架

    • Vue2
    • Vue3
    • Vue3 + TS
    • 微信小程序
    • uni-app
  • 工具与库

    • jQuery
    • Ajax
    • Axios
    • Webpack
    • Vuex
    • WebSocket
    • 第三方登录
  • 后端与语言扩展

    • ES6
    • Typescript
    • node.js
  • Element-UI
  • Apache ECharts
  • 数据结构
  • HTTP协议
  • HTTPS协议
  • 计算机网络
  • Linux常用命令
  • Windows常用命令
  • SQL数据库

    • MySQL
    • MySQL速查
  • NoSQL数据库

    • Redis
    • ElasticSearch
  • 数据库

    • MyBatis
    • MyBatis-Plus
  • 消息中间件

    • RabbitMQ
  • 服务器

    • Nginx
  • Spring框架

    • Spring6
    • SpringMVC
    • SpringBoot
    • SpringSecurity
  • SpringCould微服务

    • SpringCloud基础
    • 微服务之DDD架构思想
  • 日常必备

    • 开发常用工具包
    • Hutoll工具包
    • IDEA常用配置
    • 开发笔记
    • 日常记录
    • 项目部署
    • 网站导航
    • 产品学习
    • 英语学习
  • 代码管理

    • Maven
    • Git教程
    • Git小乌龟教程
  • 运维工具

    • Docker
    • Jenkins
    • Kubernetes
  • 算法笔记

    • 算法思想
    • 刷题笔记
  • 面试问题常见

    • 十大经典排序算法
    • 面试常见问题集锦
关于
GitHub (opens new window)
npm

(进入注册为作者充电)

  • TypeScript

    • TypeScript - 介绍
    • TypeScript - 安装和使用
    • TypeScript - 基本类型
    • TypeScript - 编译和配置
    • TypeScript - 文件打包
    • TypeScript - 接口
    • TypeScript - 函数
    • TypeScript - 类
    • TypeScript - 泛型
      • 1. 泛型介绍
      • 2. 基础示例
      • 3. 使用泛型变量
      • 4. 泛型类型
        • 泛型函数的类型
        • 泛型接口
      • 5. 泛型类
      • 6. 泛型约束
        • 使用接口约束泛型
        • 在泛型约束中使用类型参数
      • 7. 泛型工具类型
        • Partial
        • Record
        • Pick
        • Exclude
        • ReturnType
    • TypeScript 的导入导出
    • TypeScript - 类型推断
    • TypeScript - 高级类型
  • JS 超集语言 - TypeScript
  • TypeScript
scholar
2023-09-08
目录

TypeScript - 泛型

  • 1. 泛型介绍
  • 2. 基础示例
  • 3. 使用泛型变量
  • 4. 泛型类型
    • 泛型函数的类型
    • 泛型接口
  • 5. 泛型类
  • 6. 泛型约束
    • 使用接口约束泛型
    • 在泛型约束中使用类型参数
  • 7. 泛型工具类型
    • Partial
    • Record
    • Pick
    • Exclude
    • ReturnType

# 1. 泛型介绍

在软件工程中,我们不仅要创建定义良好且一致的 API,还要考虑代码的可重用性。组件不仅能够支持当前的数据类型,同时也能支持未来的数据类型,这在创建大型系统时为你提供了灵活性。

在 C# 和 Java 等语言中,可以使用泛型来创建可重用的组件。一个组件可以支持多种类型的数据,这样用户就可以以自己的数据类型来使用组件。

# 2. 基础示例

下面来创建第一个使用泛型的例子:identity 函数。这个函数会返回任何传入它的值。可以把这个函数当成是 echo 命令。

如果不用泛型的话,这个函数可能是下面这样:

// identity 函数,返回传入的数字
function identity(arg: number): number {
    return arg;
}
1
2
3
4

或者,我们可以使用 any 类型来定义函数:

// identity 函数,返回传入的任何类型的值
function identity(arg: any): any {
    return arg;
}
1
2
3
4

使用 any 类型会导致这个函数可以接收任何类型的 arg 参数,但是这样就丢失了一些信息:传入的类型与返回的类型应该是相同的。如果传入一个数字,返回值可以是任何类型。

因此,需要一种方法使返回值的类型与传入参数的类型相同。这里,我们使用了 类型变量,它是一种特殊的变量,只用于表示类型而不是值。

// 泛型函数 identity,返回与传入参数类型相同的值
function identity<T>(arg: T): T {
    return arg;
}
1
2
3
4

在这个函数中,添加了类型变量 T。T 帮助我们捕获用户传入的类型(例如:number),之后我们可以使用这个类型。这里再次使用了 T 作为返回值类型。现在可以知道参数类型与返回值类型是相同的了。这允许我们跟踪函数里使用的类型信息。

这个版本的 identity 函数称为泛型,因为它可以适用于多个类型。不同于使用 any,它不会丢失信息,像第一个例子那样保持准确性,传入数值类型并返回数值类型。

定义了泛型函数后,可以用两种方法使用。第一种是传入所有的参数,包含类型参数:

// 显式指定泛型参数为 string 类型
let output = identity<string>('myString');
1
2

这里明确指定了 T 是 string 类型,并作为一个参数传给函数,使用了 <> 括起来而不是 ()。

第二种方法更常见。利用了 类型推论:即编译器会根据传入的参数自动地帮助我们确定 T 的类型:

// 利用类型推论
let output = identity('myString');
1
2

注意没必要使用尖括号(<>)来明确地传入类型;编译器可以查看 myString 的值,然后把 T 设置为它的类型。类型推论帮助我们保持代码精简和高可读性。如果编译器不能够自动地推断出类型,就需要像上面那样明确地传入 T 的类型,在一些复杂的情况下,这是可能出现的。

# 3. 使用泛型变量

使用泛型创建像 identity 这样的泛型函数时,编译器要求在函数体中必须正确地使用这个通用的类型。换句话说,必须把这些参数当作是任意或所有类型。

来看之前 identity 的例子:

// 泛型函数 identity
function identity<T>(arg: T): T {
    return arg;
}
1
2
3
4

如果想打印出 arg 的长度,可以这样做:

// 打印传入参数的长度
function loggingIdentity<T>(arg: T): T {
    console.log(arg.length); // 错误: T 可能没有 length 属性
    return arg;
}
1
2
3
4
5

如果这么做,编译器会报错,因为使用了 arg 的 .length 属性,但没有指明 arg 具有这个属性。记住,这些类型变量代表的是任意类型,所以使用这个函数的人可能传入的是一个数字,而数字是没有 .length 属性的。

现在假设我们想操作 T 类型的数组而不直接是 T。由于我们操作的是数组,所以 .length 属性是应该存在的。我们可以像创建其它数组一样创建这个数组:

// 泛型函数 loggingIdentity,打印传入数组的长度
function loggingIdentity<T>(arg: T[]): T[] {
    console.log(arg.length); // 正确
    return arg;
}
1
2
3
4
5

可以这样理解 loggingIdentity 的类型:泛型函数 loggingIdentity,接收类型参数 T 和参数 arg,它是个元素类型是 T 的数组,并返回元素类型是 T 的数组。如果传入数字数组,将返回一个数字数组,因为此时 T 的类型为 number。这可以让我们把泛型变量 T 当作类型的一部分使用,而不是整个类型,增加了灵活性。

# 4. 泛型类型

在前面的例子中,我们创建了一个通用的 identity 函数,该函数适用于不同的类型。现在,我们研究一下函数本身的类型,以及如何创建泛型接口。

# 泛型函数的类型

泛型函数的类型与非泛型函数的类型基本相同,只是有一个类型参数在最前面,类似于函数声明:

// 定义一个泛型函数 identity
function identity<T>(arg: T): T {
    return arg;
}

// 定义一个函数类型 myIdentity,使用泛型参数 <T>
let myIdentity: <T>(arg: T) => T = identity;
1
2
3
4
5
6
7

我们还可以使用不同的泛型参数名,只要在数量和使用方式上能对应上就可以:

// 使用不同的泛型参数名 <U>,但效果相同
let myIdentity: <U>(arg: U) => U = identity;
1
2

还可以使用带有调用签名的对象字面量来定义泛型函数:

// 使用对象字面量定义泛型函数类型
let myIdentity: {<T>(arg: T): T} = identity;
1
2

# 泛型接口

使用泛型接口可以更清晰地定义函数的类型。可以将上面的对象字面量提取出来作为一个接口:

// 定义一个泛型接口
interface GenericIdentityFn {
    <T>(arg: T): T;
}

// 定义一个泛型函数 identity
function identity<T>(arg: T): T {
    return arg;
}

// 使用泛型接口定义函数类型
let myIdentity: GenericIdentityFn = identity;
1
2
3
4
5
6
7
8
9
10
11
12

还可以把泛型参数当作整个接口的一个参数。这样接口里的其它成员也能知道这个参数的类型了:

// 定义一个泛型接口,接口本身接收泛型参数
interface GenericIdentityFn<T> {
    (arg: T): T;
}

// 定义一个泛型函数 identity
function identity<T>(arg: T): T {
    return arg;
}

// 使用泛型接口定义函数类型,并指定泛型参数为 number
let myIdentity: GenericIdentityFn<number> = identity;
1
2
3
4
5
6
7
8
9
10
11
12

注意,这样的接口定义了一种约定,使得函数在调用时必须使用相同的泛型参数类型。

除了泛型接口,还可以创建泛型类。注意,无法创建泛型枚举和泛型命名空间。

# 5. 泛型类

泛型类与泛型接口类似。泛型类使用尖括号 (<>) 括起泛型类型,跟在类名后面。

// 定义一个泛型类 GenericNumber
class GenericNumber<T> {
    zeroValue: T;
    add: (x: T, y: T) => T;
}

// 创建一个 GenericNumber 的实例,泛型参数为 number
let myGenericNumber = new GenericNumber<number>();
myGenericNumber.zeroValue = 0;
myGenericNumber.add = function(x, y) {
    return x + y;
};
1
2
3
4
5
6
7
8
9
10
11
12

GenericNumber 类的使用是十分直观的,并且没有限制它只能使用 number 类型。也可以使用字符串或其它更复杂的类型。

// 创建一个 GenericNumber 的实例,泛型参数为 string
let stringNumeric = new GenericNumber<string>();
stringNumeric.zeroValue = '';
stringNumeric.add = function(x, y) {
    return x + y;
};

console.log(stringNumeric.add(stringNumeric.zeroValue, 'test')); // 输出 'test'
1
2
3
4
5
6
7
8

与接口一样,直接把泛型类型放在类后面,可以帮助我们确认类的所有属性都在使用相同的类型。

在 TypeScript 的类中,类有两部分:静态部分和实例部分。泛型类指的是实例部分的类型,所以类的静态属性不能使用这个泛型类型。

# 6. 泛型约束

有时候,我们想要操作某类型的一组值,并且我们知道这组值具有什么样的属性。在 loggingIdentity 例子中,我们想访问 arg 的 length 属性,但是编译器并不能证明每种类型都有 length 属性,所以就报错了。

// 试图访问泛型参数的 length 属性,会导致编译错误
function loggingIdentity<T>(arg: T): T {
    console.log(arg.length); // 错误: T 可能没有 length 属性
    return arg;
}
1
2
3
4
5

相比于操作 any 所有类型,我们想要限制函数去处理任意带有 .length 属性的所有类型。只要传入的类型有这个属性,我们就允许。为此,我们需要列出对于 T 的约束要求。

# 使用接口约束泛型

可以定义一个接口来描述约束条件,创建一个包含 .length 属性的接口,并使用这个接口和 extends 关键字来实现约束:

// 定义一个接口,描述具有 length 属性的类型
interface Lengthwise {
    length: number;
}

// 使用接口约束泛型 T,要求 T 具有 length 属性
function loggingIdentity<T extends Lengthwise>(arg: T): T {
    console.log(arg.length); // 正确
    return arg;
}

// 错误,因为 3 是数字,不具有 length 属性
loggingIdentity(3);

// 正确,传入的对象具有 length 属性
loggingIdentity({length: 10, value: 3});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

# 在泛型约束中使用类型参数

可以声明一个类型参数,并让它被另一个类型参数所约束。例如,现在想要用属性名从对象里获取这个属性,并且确保这个属性存在于对象 obj 上,因此需要在这两个类型之间使用约束。

// 使用两个类型参数 T 和 K,其中 K 被约束为 T 的键类型
function getProperty<T, K extends keyof T>(obj: T, key: K) {
    return obj[key];
}

let x = {a: 1, b: 2, c: 3, d: 4};

// 正确,因为 'a' 是 x 的键
getProperty(x, 'a');

// 错误,因为 'm' 不是 x 的键
getProperty(x, 'm');
1
2
3
4
5
6
7
8
9
10
11
12

在这个例子中,keyof T 表示 T 类型的所有属性名构成的联合类型。通过这种方式,可以在泛型约束中使用类型参数来实现更精确的类型检查。

# 7. 泛型工具类型

TypeScript 提供了一些内置的泛型工具类型,用于对类型进行转换和操作。这些工具类型可以帮助我们简化代码,并在类型级别上进行更精细的控制。

# Partial

Partial<T> 的作用是将某个类型 T 中的所有属性变为可选属性。这在处理不完整的数据对象时非常有用。

示例:

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

// 使用 Partial 将 Person 类型的属性全部变为可选
function student<T extends Person>(arg: Partial<T>): Partial<T> {
    return arg;
}

const studentInfo: Partial<Person> = {
    name: 'John'
    // age 属性可以不存在
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

在上面的示例中,student 函数接收一个 Partial<T> 类型的参数,意味着传入的对象可以只包含 Person 类型的一部分属性。

# Record

Record<K extends keyof any, T> 的作用是将键类型 K 中的所有属性转换为类型 T。这是创建一个映射类型的快捷方式。

示例:

interface PageInfo {
    title: string;
}

// 使用 Record 创建一个映射类型,将 Page 的键映射到 PageInfo 类型
type Page = 'home' | 'about' | 'other';

const x: Record<Page, PageInfo> = {
    home: { title: "Home Page" },
    about: { title: "About Page" },
    other: { title: "Other Page" },
};
1
2
3
4
5
6
7
8
9
10
11
12

在上面的示例中,Record<Page, PageInfo> 表示一个对象,其中的每个键(Page 类型的值)都映射到一个 PageInfo 对象。

# Pick

Pick<T, K extends keyof T> 的作用是从类型 T 中挑选出某些属性,生成一个新的类型。这对于创建类型的子集非常有用。

示例:

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

// 使用 Pick 从 Todo 类型中挑选出 title 和 time 属性
type TodoPreview = Pick<Todo, 'title' | 'time'>;

const todo: TodoPreview = {
    title: '吃饭',
    time: '明天'
};
1
2
3
4
5
6
7
8
9
10
11
12
13

在上面的示例中,TodoPreview 类型仅包含 Todo 类型的 title 和 time 属性。

# Exclude

Exclude<T, U> 的作用是从类型 T 中排除那些可以分配给类型 U 的属性。它用于从联合类型中去除某些成员。

示例:

// 使用 Exclude 从 "a" | "b" | "c" 中排除 "a"
type T0 = Exclude<"a" | "b" | "c", "a">;

const t: T0 = 'b'; // T0 的类型是 "b" | "c"
1
2
3
4

在上面的示例中,Exclude<"a" | "b" | "c", "a"> 的结果是 b | c,因为 a 被排除掉了。

# ReturnType

ReturnType<T> 的作用是用于获取函数 T 的返回类型。这在需要动态获取函数返回类型时非常有用。

示例:

// 定义各种返回类型的函数
type T0 = ReturnType<() => string>; // string
type T1 = ReturnType<(s: string) => void>; // void
type T2 = ReturnType<<T>() => T>; // {}
type T3 = ReturnType<<T extends U, U extends number[]>() => T>; // number[]
type T4 = ReturnType<any>; // any
type T5 = ReturnType<never>; // any

// 错误示例:以下两行会报错,因为 ReturnType 需要一个函数类型
// type T6 = ReturnType<string>; // Error
// type T7 = ReturnType<Function>; // Error
1
2
3
4
5
6
7
8
9
10
11

在上面的示例中,ReturnType 用于获取不同函数的返回类型。需要注意的是,ReturnType 只能用于函数类型,而不能用于普通的类型。

这些泛型工具类型是 TypeScript 提供的强大功能,可以帮助我们更好地处理复杂的类型操作和转换。它们极大地提高了代码的灵活性和可维护性。

编辑此页 (opens new window)
上次更新: 2024/12/28, 18:32:08
TypeScript - 类
TypeScript 的导入导出

← TypeScript - 类 TypeScript 的导入导出→

Theme by Vdoing | Copyright © 2019-2025 程序员scholar
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式