TypeScript学习笔记(七) 泛型

大家好,我是半虹,这篇文章来讲 TypeScript 中的泛型


1、概述

有些时候,定义函数或者类时,可能无法事先确定其中参数的类型

举个例子,假设现在有个函数,这个函数接收一个值再返回这个值

实际上这个值可以是任意类型,我们无法事先确定,因此只好设为 any

function identity(value:any):any {return value;
}

但是,这种写法无法反映参数和返回类型间的关系,理论上二者是相同的

为此,这个时候就要用到泛型!


所谓的泛型其实就是参数化的类型,具体有两特点:

  1. 定义的时候声明参数表示类型(定义时声明),具体是在 <> 内声明
  2. 使用的时候参数绑定实际类型(使用时绑定),同样是在 <> 内绑定
// 定义
// 声明泛型参数,这里参数声明为 T (参数名称可任意指定)
function identity<T>(value:T):T {return value;
}// 使用
// 绑定实际类型,这里分别绑定为 number 和 string
identity<number>(123);  // 相当于:function identity(value:number):number { ... }
identity<string>('0');  // 相当于:function identity(value:string):string { ... }

定义泛型时就像声明一个占位类型,而在使用时会把占位类型替换成实际类型

使用泛型有助于提高代码的重用性,允许不确定类型时,依然可以有类型约束


2、基本语法

泛型可以应用于不同的场景,这些场景具体包括:函数、类、接口、类型别名

不同的场景中,都遵循一个规则:定义时声明泛型参数,使用时绑定泛型参数

定义时,在 <> 内声明泛型参数,如果存在有多个参数,那么需要用逗号隔开

使用时,在 <> 内绑定泛型参数,如果存在有多个参数,那么则要按顺序指定

但是不同的场景定义时声明参数的位置以及参数的作用范围也不同,具体如下:


(1)泛型函数

我们知道,定义函数有两种方式,一是函数声明,二是函数表达式

  1. 如果是函数声明,  泛型参数声明写在函数名称之后
  2. 如果是函数表达式,泛型参数声明写在类型签名之中,而类型签名有两种
    1. 如果是简写版类型签名,泛型参数声明写在整条声明前面
    2. 如果是完整版类型签名,泛型参数声明写在每条声明前面

声明的类型参数可以在函数之内的任意位置引用

无论哪一种定义方式,使用函数时泛型参数绑定都在函数名称之后

// 定义函数// 函数声明
// 格式如下:
// function func_name<gene_name1, ...>(prop1:type1, ...):return_type { ... }
// 例子如下:
function reverseArray0<E>(items: E[]): E[] {return items.reverse();
}// 函数表达式(简写版类型签名)
// 格式如下:
// <gene_name1, ...>(prop1:type1, ...) => return_type
// 例子如下:
let reverseArray1: <E>(items: E[]) => E[];
reverseArray1 = reverseArray0;// 函数表达式(完整版类型签名)
// 格式如下:
// {
//   <gene_name1, ...>(prop1:type1, ...): return_type;
//   ...
// };
// 例子如下:
let reverseArray2: { <E>(items: E[]): E[]; }
reverseArray2 = reverseArray0;// 使用函数// 调用函数
reverseArray0<number>([123, 456, 789]); // 绑定泛型参数为:number
reverseArray0<string>(['1', '4', '7']); // 绑定泛型参数为:string
reverseArray1<number>([234, 567, 891]);
reverseArray1<string>(['2', '5', '8']);
reverseArray2<number>([345, 678, 912]);
reverseArray2<string>(['3', '6', '9']);

(2)泛型类

而对于类来说,声明和绑定泛型参数都写在类名之后

声明泛型参数后可以在整个类中引用,除了静态成员,因为泛型类是描述类的实例

绑定泛型参数则发生在使用类的时候,具体包括实例化对象以及类的继承等的操作

类静态成员在类加载时就已经存在,且不依赖类的实例化过程

而泛型参数在类使用时才具体确定,因此无法被静态成员引用

// 定义类class Container<T> {// 普通属性value: T;// 构造函数constructor(value: T) {this.value = value;}// 普通方法setValue(value: T) {this.value = value;}getValue(): T {return this.value;}convert<U>(f:(input:T) => U): U { // 类中方法也可声明泛型参数,此处声明的参数只能在该方法内引用return f(this.value);}
}// 使用类// 实例化对象
let container1 = new Container<number>(123); // 绑定泛型参数为:number
let container2 = new Container<string>('0'); // 绑定泛型参数为:string
container1.setValue(456);
container2.setValue('1');
container1.convert<string>(num => num.toString()); // 调用方法时,绑定泛型参数为:string
container2.convert<string>(str => str.toString()); // 调用方法时,绑定泛型参数为:string// 继承类
class Extended1 extends Container<number> {  // 绑定泛型参数为:number (实际类型)compare(input:number):boolean {return this.value === input;}
}
class Extended2<NT> extends Container<NT> {  // 绑定泛型参数为:NT     (泛型参数)compare(input:NT):boolean {return this.value === input;}
}
let container3 = new Extended1(123);         // 无需绑定
let container4 = new Extended2<string>('0'); // 绑定泛型参数为:string
container3.setValue(456);
container4.setValue('1');
container3.convert<string>(num => num.toString()); // 调用方法时,绑定泛型参数为:string
container4.convert<string>(str => str.toString()); // 调用方法时,绑定泛型参数为:string
container3.compare(1234);
container4.compare('00');

(3)泛型接口

接口可以用于描述对象的属性和方法,此时声明和绑定泛型参数都写在接口名后

使用接口通常包括以下的场景:接口作为类型、类实现接口、接口继承接口等等

// 定义接口interface SimpleGuest<T> { // 访客接口id: T;name: string;
}interface SimpleCache<K, V> { // 缓存接口set(key: K, val: V): void;get(key: K): V | undefined;
}// 使用接口// 作为类型
let guest1:SimpleGuest<number> = { id: 123, name: '1' };
let guest2:SimpleGuest<number> = { id: 456, name: '4' };
let guest3:SimpleGuest<number> = { id: 789, name: '7' };// 实现接口
class GuestCache implements SimpleCache<number, string> {private keys: number[] = [];private vals: string[] = [];// 实现 setset(key: number, val: string): void {const index = this.keys.indexOf(key);if (index !== -1) {this.vals[index] = val;} else {this.keys.push(key);this.vals.push(val);}}// 实现 getget(key: number): string | undefined {const index = this.keys.indexOf(key);if (index !== -1) {return this.vals[index];} else {return undefined;}}
}
let cache0 = new GuestCache();
cache0.set(guest1.id, guest1.name);
cache0.set(guest2.id, guest2.name);
cache0.get(guest2.id); // 4
cache0.get(guest3.id); // undefined// 继承接口
interface DetailCache<K, V> extends SimpleCache<K, V> {remove(key: K): void;clear(): void;size(): number;
}

(4)泛型类型别名

类型别名和接口十分相似,同样也是:声明和绑定泛型参数都写在类型别名之后

类型别名和接口不同的是,类型别名除了可以定义对象类型,还能定义任意类型

// 定义类型别名type Result<T> = T | Error; // 非对象类型type TreeNode<T> = { // 对象类型value: T;children: TreeNode<T>[];
}// 使用类型别名function handleResult<T>(result: Result<T>): void {if (result instanceof Error) {// ...} else {// ...}
}
handleResult<number>(123);
handleResult<string>(new Error('something went wrong'));let node: TreeNode<number> = {value: 1,children: [{value: 2,children: [],}, {value: 3,children: [],}]
}

3、泛型推导

上面的例子中,我们在使用泛型参数时都是显式绑定实际类型

实际上对于函数和类来说,编译器也能根据传入参数自动推导

// 定义泛型函数
function identity<T>(value: T): T {return value;
}
function reverseArray<T>(items: T[]): T[] {return items.reverse();
}// 定义泛型类
class Container<T> {public value: T;constructor(value: T) {this.value = value;}
}// 使用泛型函数
identity(123);                       // 此时自动推导 T 为 123
identity('0');                       // 此时自动推导 T 为 '0'
reverseArray([123, 456, 789]);       // 此时自动推导 T 为 number
reverseArray(['1', '4', '7']);       // 此时自动推导 T 为 stirng// 使用泛型类
let container1 = new Container(123); // 此时自动推导 T 为 number
let container2 = new Container('0'); // 此时自动推导 T 为 string

但是有些时候,编译器可能无法推导出正确的类型,这时就要显式绑定

function combineArray<T>(arr1: T[], arr2: T[]): T[] {return [...arr1, ...arr2];
}
combineArray([123, 456, 789], ['1', '4', '7']);                 // 隐式推导,编译错误
combineArray<number|string>([123, 456, 789], ['1', '4', '7']);  // 显式绑定,编译正常

另外需要注意,所有的泛型参数要么都有显式绑定,要么都不显式绑定

function combineArray<U, V>(arr1: U[], arr2: V[]): (U | V)[] {return [...arr1, ...arr2];
}
combineArray([123, 456, 789], ['1', '4', '7']);                 // 都不绑定,编译正常
combineArray<number, string>([123, 456, 789], ['1', '4', '7']); // 都有绑定,编译正常
combineArray<number>([123, 456, 789], ['1', '4', '7']);         // 部分绑定,编译错误

4、泛型约束

有些时候,仅声明泛型参数可能还不够,我们还希望表达这个泛型参数满足某种约束

举个例子,有一个打印数组元素的函数,操作是遍历数组然后调用元素  print 方法

// 遍历数组,调用元素中的 print 方法进行打印function printArray<T>(values:T[]): void {values.forEach(item => item.print()); // 编译错误
}

但是上述写法编译时会报错,因为无法保证数组元素具有  print 方法

这个时候就要用到泛型约束,确保泛型参数满足某种约束

具体可在泛型参数后加  extends 关键字,并在其后写明具体约束条件

例如 T extends U 可理解为 TU 的子类型,T 至少应该为 U

// 通过 T extends { print() : void } 确保 T 至少应该具有 print 方法function printArray<T extends { print() : void }>(values:T[]): void {values.forEach(item => item.print()); // 编译正常
}

如果存在多个泛型参数,那么一个参数可以引用其它参数作为约束条件

但是这里需要注意一点,就是这些参数之间不能循环引用,否则会报错

// 编译正常,引用其它参数
function getProperty<O, K extends keyof O>(obj: O, key: K): O[K] { // 获取对象属性的常用泛型函数return obj[key];
}// 编译错误,存在循环引用
function test1<U extends U>() {}
function test2<U extends V, V extends U>() {}

5、默认类型

就像函数参数指定默认值一样,声明泛型参数的时候也能指定默认类型

带有默认类型的泛型参数必须在没有默认类型的泛型参数之后

function makePair1<U, V = string>(first: U, second: V): [U, V] { // 编译正常return [first, second];
}function makePair2<U = number, V>(first: U, second: V): [U, V] { // 编译错误return [first, second];
}

带有默认类型的泛型参数使用时遵循以下规则:

  1. 如有显式指定的参数,此时:
    1. 已被指定的参数,就要用指定的类型覆盖默认的类型
    2. 没被指定的参数,就要用默认的类型
  2. 如无显式指定的参数,此时:
    1. 如果能隐式推导,则会用推导的类型覆盖默认的类型
    2. 如不能隐式推导,则会用默认的类型
function makePair0<U = number, V = string>(first: U, second: V): [U, V] {return [first, second];
}// 如有显式指定的参数:// 第一个参数已被指定,使用指定的类型覆盖默认的类型,即 string
// 第二个参数已被指定,使用指定的类型覆盖默认的类型,即 number
makePair0<string, number>('0', 123);// 第一个参数已被指定,使用指定的类型覆盖默认的类型,即 string
// 第二个参数没被指定,使用默认的类型,即 string【注意】
makePair0<string>('0', '0');// 如无显式指定的参数:// 此时可以做隐式推导,使用推导的类型覆盖默认的类型
makePair0('0', 123);  // 第一个参数为 number,第二个参数为 string
makePair0('0', '0');  // 第一个参数为 string,第二个参数为 string


好啦,本文到此结束,感谢您的阅读!

如果你觉得这篇文章有需要修改完善的地方,欢迎在评论区留下你宝贵的意见或者建议

如果你觉得这篇文章还不错的话,欢迎点赞、收藏、关注,你的支持是对我最大的鼓励 (/ω\)

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/diannao/16014.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

01-03.Vue:v-on的事件修饰符

01-03.Vue&#xff1a;v-on的事件修饰符 前言v-on的事件修饰符.stop的举例.capture举例.prevent的举例1.prevent的举例2.self举例 前言 我们接着上一篇文章 01-02.Vue的常用指令(二) 来讲 下一篇文章 01-04.Vue的使用示例&#xff1a;列表功能 v-on的事件修饰符 v-on 提供了很…

如何使用Python中的map()、filter()和reduce()函数

在Python中&#xff0c;map(), filter(), 和 reduce() 是内置的高阶函数&#xff08;或称为函数式编程工具&#xff09;&#xff0c;它们允许你对可迭代对象&#xff08;如列表、元组等&#xff09;进行批量操作。以下是这三个函数的基本用法和示例。 map() map() 函数接受一…

AI语音及其应用

文章目录 一、基本认识二、AI语音应用场景三、真人录音与AI配音的区别四、AI语音创作基本步骤 本文将简单了解下AI语音、应用场景及其优势和创作核心步骤。 一、基本认识 AI语音是指基于人工智能技术开发的语音识别和语音合成系统。语音识别是指计算机识别和理解人类语音的能力…

【机器智能】:AI机器学习在医疗服务的广泛应用与实践案例

目录 引言一&#xff0c;什么是机器学习二&#xff0c;AI在医学影像诊断中的应用三&#xff0c;AI在个性化治疗方案设计中的应用四&#xff0c;医疗图像识别技术五&#xff0c;医疗语言识别技术六&#xff0c;结语 引言 随着人工智能&#xff08;AI&#xff09;和机器学习技术…

SQL注入:原理及示例讲解,配置mysql环境变量,pikachu靶场搭建

SQL注入原理 SQL注入&#xff08;SQL Injection&#xff09;是一种代码注入技术&#xff0c;攻击者通过将恶意的SQL代码插入到应用程序的输入字段中&#xff0c;诱使后台数据库执行这些恶意代码&#xff0c;从而对数据库进行未授权的操作。常见的操作包括获取敏感数据、篡改数…

知能行——考研数学利器

知能行使用体验全记录 首先&#xff0c;我先介绍一下自己&#xff0c;我是2018级的&#xff0c;2022年6月毕业&#xff0c;本科沈阳工业大学&#xff08;双非&#xff09;&#xff0c;今年二战&#xff0c;专业课自动控制原理&#xff0c;数二英二&#xff0c;目标是江南大学控…

[书生·浦语大模型实战营]——第三节:茴香豆:搭建你的 RAG 智能助理

0.RAG 概述 定义&#xff1a;RAG&#xff08;Retrieval Augmented Generation&#xff09;技术&#xff0c;通过检索与用户输入相关的信息片段&#xff0c;并结合外部知识库来生成更准确、更丰富的回答。解决 LLMs 在处理知识密集型任务时可能遇到的挑战, 如幻觉、知识过时和缺…

社交媒体数据恢复:聊天宝

请注意&#xff0c;本教程仅针对聊天宝应用程序&#xff0c;而非其他聊天软件。以下是详细的步骤&#xff1a; 首先&#xff0c;请确保您已经登录了聊天宝应用程序。如果您尚未登录&#xff0c;请使用您的账号登录。 在聊天宝主界面&#xff0c;找到您希望恢复聊天记录的对话框…

小明同学的考试分数统计:总分、平均分与方差计算进阶

新书上架~&#x1f447;全国包邮奥~ python实用小工具开发教程http://pythontoolsteach.com/3 欢迎关注我&#x1f446;&#xff0c;收藏下次不迷路┗|&#xff40;O′|┛ 嗷~~ 目录 一、原始方法介绍与问题发现 原始方法存在的问题 二、优化方案&#xff1a;使用列表简化代…

串口中断原理及实现

一、串口的原理 SM0、SM1——串行口工作模式 SM0SM1模式特点00模式0移位寄存器方式&#xff0c;用于I/O口扩展01模式18位UART,波特率可变10模式29位UART,波特率为时钟频率/32或/6411模式39位UART,波特率可变 TI、RI——发送、接收中断标志位 TITI0 允许发送>TI1 发送完成后…

【全网最全】2024电工杯数学建模A题54页A题保奖成品论文+配套代码

您的点赞收藏是我继续更新的最大动力&#xff01; 一定要点击如下的卡片链接&#xff0c;那是获取资料的入口&#xff01; 【全网最全】2024电工杯数学建模A题成品论文前三题完整解答matlabpy代码等&#xff08;后续会更新成品论文&#xff09;「首先来看看目前已有的资料&am…

出书,是「盖你自己的房子」你知道吗?

出书是「盖你自己的房子」 尊敬的出书盟友&#xff1a; 你好&#xff01;我希望这封信能够激发您对出书和阅读的热情。 在当今信息爆炸的时代&#xff0c;每个人都有机会分享自己的故事、思想和知识。而书籍作为一种流传百年的媒体&#xff0c;依旧承载着无限的力量和影响力…

设计原则-

在设计领域&#xff0c;有许多被广泛接受和推崇的设计原则。以下是五个重要的设计原则&#xff08;这些原则不仅适用于图形设计&#xff0c;也适用于用户体验设计、交互设计、产品设计等&#xff09;&#xff1a; 一致性原则&#xff08;Consistency&#xff09;&#xff1a; …

Java——接口后续

1.Comparable 接口 在Java中&#xff0c;我们对一个元素是数字的数组可以使用sort方法进行排序&#xff0c;如果要对一个元素是对象的数组按某种规则排序&#xff0c;就会用到Comparable接口 当实现Comparable接口后&#xff0c;sort会自动调用Comparable接口里的compareTo 方法…

sklearn基础教程

scikit-learn是一个用于机器学习的Python库&#xff0c;提供了多种机器学习的方法和模型&#xff0c;以及数据预处理、特征选择、模型评估等功能。它简化了机器学习流程&#xff0c;并且具有易于使用和灵活的特点。 本教程将介绍sklearn的基础知识和常用功能&#xff0c;帮助你…

C++的类和对象

C面向对象的三大特性&#xff1a;封装&#xff0c;继承&#xff0c;多态 万事万物皆可为对象&#xff0c;有其相应的属性和行为 一、封装 1.1 封装的意义 将属性和行为作为一个整体&#xff0c;表现生活中的事物 将属性和行为加以权限控制 在设计类的时候&#xff0c;属性…

Sql语句DQL操作 查询操作单表 多表 子表(嵌套)

DQL 查询语句 查询指定的列 **语法 : ** SELECT [查询列表] FROM 表名结果可以是:表格中的字段,常量,表达式,函数查询的结果是虚拟表格,不可以操作 是只读的可以对查询结果进行 算术运算( - * /);**特点: ** 查询的列表可以是:表中的字段,常量,表达式,函数查询的结果是一个虚…

集合转为树结构工具

表头 工具类 public class ThreeUtils {/*** 树结构转换处理-每次递归查询全部下级以及下级的子集** param menuList 需要处理的数据集* param threeResult 返回对象* param parentId 父级ID* param dataTreating 逻辑处理* param <T>*/public static <T ex…

深入理解 Spring 上下文(Context)层次结构

前言 在使用 Spring 框架进行应用程序开发时&#xff0c;Spring 上下文&#xff08;Context&#xff09;是一个非常重要的概念。Spring 上下文提供了一个环境&#xff0c;用于管理应用程序中的对象&#xff08;通常称为 Bean&#xff09;及其之间的依赖关系。在复杂的应用程序…