TypeScript 基础学习之泛型和 extends 关键字

越来越多的团队开始使用 TS 写工程项目, TS 的优缺点也不在此赘述,相信大家都听的很多了。平时对 TS 说了解,仔细思考了解的也不深,借机重新看了 TS 文档,边学习边分享,提升对 TS 的认知的同时,也希望能在平时的工作中能用上,少写一点 any。

泛型
A major part of software engineering is building components that not only have well-defined and consistent APIs, but are also reusable. Components that are capable of working on the data of today as well as the data of tomorrow will give you the most flexible capabilities for building up large software systems.
In languages like C# and Java, one of the main tools in the toolbox for creating reusable components is generics, that is, being able to create a component that can work over a variety of types rather than a single one. This allows users to consume these components and use their own types.
我们工作的大部分内容是构建组件,定义有效、一致并且可复用的API很重要。组件能够处理当前的数据,又能考虑兼容很多未来的数据,这样的组件能提高工作效率,也能在构建软件系统的时候提供非常灵活的能力。
▐ 基本使用
通过泛型可以定义通用的数据结构,增加 TypeScript 代码中类型的通用性。
处理函数
先看一个具体的例子,感受一下泛型的应用。
首先定一个 log 函数,功能很简单把传入的参数直接 return 就行,函数参数类型是 string,那么返回值也是 string 类型。
function log(arg: string): string {return arg;
}当其他地方也想使用这个函数,但是参数入参数类型是 number,这个时候我们也许可以这么做:
function log(arg: string | number): string | number {return arg;
}当有更多的地方要使用这个函数的时候,那这个函数的参数类型定义和返回值类型定义将会变得无比冗长,或者可能就直接使用 any 来解决,当使用 any 的时候就失去了使用 TS 最初的初心了。
这个时候泛型出现了,能解决输入输出一致的问题,我们可以这样写:
function log<T>(arg: T): T {return arg;
}这个 log 函数通过泛型来约束输入输出一致性的问题,把动态的泛型类型抛给函数的使用者,我们只需要保证输入输出的一致性就可以,还能支持任何类型。泛型中的 T 就像一个占位符,或者说一个变量,在使用的时候把定义的类型像参数一样传入就可以了。
我们在使用的时候可以有两种方式指定类型。第一,是直接定义要使用的类型,第二,是默认 TS 的类型推断,TS自动推导出要传入的类型:
log<string>('log') // 定义 T 为 stringprint('log') // TS 的类型推断,自动推导类型为 T 的类型为 string默认参数
在 JS 中对于一个函数入参,可以使用默认参数来简化当没有传参数时候的默认值,在 TS 中我们可以这样使用:
function log<T = string>(arg: T): T {return arg;
}当没有传泛型参数的时候 T 的默认类型是 string 类型,就如果 JS 中的函数默认参数类似的用法
多个参数
当函数中有多个参数的时候可以这样使用:
function log<T, U>(type: T, info: U): [T, U] {return [type, info];
}通过在泛型定义多个对应位置的类型就可以获取到相应的泛型传参来对输入输出做一些处理。
函数返回值
泛型不仅可以很方便地约束函数的参数类型,还可以用在函数执行副作用操作的时候。发送请求是我们使用的很多的操作,我们会有一个通用的发送请求的异步方法,请求不同的 url 会返回不同的类型数据,那么我们可以这样使用。
function request<T>(url: string): Promise<T> {return fetch(url).then(res => res.json())
}interface IUserInfo {name: string;age: number;avatar: string;gender: 'male' | 'female';city: string;
}request<IUserInfo>('/getuserinfo').then(res => {console.log(res)
});这个时候返回的数据 TS 就会识别出 res 的类型,对解下来的代码编写会有很大帮助。
▐ 应用
上面的一些小例子,我们对泛型有了一些了解,在平时我们可以这样使用。
泛型约束类
定一个栈,有出栈和入栈两个方法,规定了出栈和入栈的元素类型必须一致。
class Stack<T> {private data: T[] = []push(item:T) {return this.data.push(item)}pop(): T | undefined {return this.data.pop()}
}在使用的使用传入类型生成实例,在调用对应的出栈和入栈方法的时候入参数类型不对就会报错,从而约束了栈的元素类型。const test1 = new Stack<number>()
const test2 = new Stack<string>()泛型约束接口
function request<T>(url: string): Promise<T> {return fetch(url).then(res => res.json())
}interface IUserInfo<T> {name: string;age: number;avatar: string;gender: 'male' | 'female';address: T;
}request<IUserInfo<string>>('/getuserinfo').then(res => {console.log(res)
});这样使用 IUserInfo 的 address 的类型就是 string,让 interface 更灵活。
Pick
Pck 是 TS 内置的函数,作用是挑选出对象类型 T 中 U 对应的属性和类型,创建一个新的对象类型。
type Pick<T, K extends keyof T> = {[P in K]: T[P];
};interface IUserInfo {name: string;age: number;avatar: string;gender: 'male' | 'female';
}type Test = Pick<IUserInfo, 'name'> // { name: string }Omit
与Pick的功能是互补的,挑选出对象类型 T 中不在 U 中的属性和类型,创建一个新的对象类型。
type Omit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>;interface IUserInfo {name: string;age: number;avatar: string;gender: 'male' | 'female';
}type Test = Omit<IUserInfo, 'name' | 'avatar' | 'gender'> // { age: number }▐ 总结
泛型,从字面上来理解,就是一般的,广泛的,具有通用性的。
泛型是指在定义函数、接口或类的时候,不预先指定具体类型,而是在使用的时候再指定类型。
泛型中的 T 就像一个占位符、或者说一个变量,在使用的时候可以把定义的类型像参数一样传入,它可以原封不动地输出。

extends
本文主要整理 extends 关键字在 typescript中的相关用法,平时在看一些复杂的 TS 类型的时候经常会看到使用 extends 这个关键字
▐ 继承类型
TS 中的 extends 关键字第一个用法可以理解成 JS 中相似的用法继承类型。interface IName { name: string;
}interface IGender {gender: string;
}interface IPerson extends IName, IGender {age: number;
}const corgi: IPerson = {name: 'corgi',gender: 'female',age: 18,
}以上示例中,IName 和 IGender 两个接口,分别定义了 name 属性和 gender 属性,IPerson 则使用extends 关键字多重继承的方式,继承了 IName 和 IGender,同时定义了自己的属性age,此时 IPerson 除了自己的属性外,还同时继承了 IName 和 IGender 的属性。
▐ 条件判断
When the type on the left of the extends is assignable to the one on the right, then you’ll get the type in the first branch (the “true” branch); otherwise you’ll get the type in the latter branch (the “false” branch).
当 extends 左边的类型可以赋值给右边的类型时,你会在第一个分支中获取获得这个类型(true),否你会在第二个个分支中获得这个类型(false)
普通条件类型
先来直接看个例子
// 示例1
interface IAnimal {name: string;
}interface IDog extends IAnimal {color: string;
}// A的类型为string
type Test = IDog extends IAnimal ? string : number;extends 的条件判断和 JS 中的三元表达式很类似,如果问号前面的条件为真就把 string 类型赋值给 A,否则就把 number 类型赋值给 A。那么问号前面的条件判断真假的逻辑是什么呢?就像上面的那段英文描述一样,当extends 左边的类型可以赋值给右边的类型的时候,就会真,否则为假。
在上面的例子中,IDog 是 IAnimal 的子类,子类比父类的限制更多,如果能满足子类的条件约束,就一定能满足父类的条件约束,IDog 类型的值可以满足 IAnimal 类型,判断结果为真,Test 的类型为 string。
再来一个例子:
// 示例2
interface I1 {name: string
}interface I2 {name: stringage: number
}
// A的类型为string
type Test = I2 extends I1 ? string : number这个例子,代入上面的解法来看就是,能满足 I2 类型约束的值也满足 I1 类型约束,判断结果为真,Test 的类型为 string。
多看几个例子:
type Test1 = 'x' extends 'x' ? "true" : "false"; // "true"
type Test2 = 'x' extends 'y' ? "true" : "false" // "false"
type Test3 = 100 extends 100 ? "true" : "false" // "true"
type Test4 = 200 extends 100 ? "true" : "false" // "false"
type Test5 = {} extends {name:string} ? "true" : "false" // "false"
type Test6 = {name:string} extends {} ? "true" : "false" // "true"按照上面的解释能够很好的解释出最后的结果。
分配条件类型
再多看几个例子:
type Test1 = 'x' extends 'x' ? string : number; // string
type Test2 = 'x' | 'y' extends 'x' ? string : number; // numbertype P<T> = T extends 'x' ? string : number;
type Test3 = P<'x' | 'y'> // tring | numbertype P<T> = 'x' extends T ? string : number;
type Test4 = P<'x' | 'y'> // tring这里就先把最后的结果直接给出来了,看到 Test1、Test2 和 Test4 还能理解,但是 Test3 的结果为什么就是 string |nunber这个类型了呢?同样的按照泛型传参数,按照直觉来说,Test3 和 Test2 应该是一样的结果,为什么结果差异这么大呢?
这里导致结果和直觉不一样的原因就是所谓的分配条件类型。
When conditional types act on a generic type, they become distributive when given a union type
当 extends 前面的参数是一个泛型类型,当传入的参数是一个联合类型的时候,就是使用分配律计算最后的结果,分配律就是我们从数学中学到的分配律。把联合类型中的每个类型代入条件判断得到每个类型的结果,再把每个类型的结果联合起来,得到最后的类型结果。
那么就可以按照这个解法来代入 Test3 的解释原因:
extends 前面的参数 T 是泛型参数,Test3 中 泛型代入的的 x | y这个联合类型,这个时候就触发了分配条件类型,来使用分配律
'x' extends 'x' ? string : number; // string
'y' extends 'x' ? string : number; // number
type Test3 = string | number按照分配条件来看最后的结果恍然大悟,总之要触发分配条件类型要满足两个条件,第一,extends 前面的参数是泛型类型,第二,参数是联合类型。
条件分配类型是系统默认的行为,那么在某些需求不想要出发条件分配类型应该怎么办呢?
看下面的例子:
type P<T> = [T] extends ['x'] ? string : number;
type Test = P<'x' | 'y'> // number这个使用使用了[]这个符号把泛型类型参数包起来,这个时候 extends 前面的参数就变成这个样子['x' | 'y'],不满足触发分配条件类型的条件,按照普通条件来判断,得到最后的结果为 number。
never
来个例子看看:
// never是所有类型的子类型
type Test1 = never extends 'x' ? string : number; // stringtype P<T> = T extends 'x' ? string : number;
type Test2 = P<never> // never上面直接给出了最后的结果,但是为什么看起来 Test2 最后的结果又和直觉中不太一样,never 不是联合类型,直接代入条件类型之后,按理来说 Test2 和 Test1 的结果应该一样才对。
事实上,never 被认为是空的联合类型,也就是没有任何项的联合类型,所以还是满足上面的分配条件类型,因为没有任何联合项可以分配,所以P<T>根本就没有执行,就和永远没有返回的函数一样,属于 never 类型。
按照上面的条件,可以这样子来阻止分配条件类型。
type P<T> = [T] extends ['x'] ? string : number;
type Test = P<never> // string▐ 应用
Exclude
type Exclude<T, U> = T extends U ? never : T;Exclude 是 TS 内置的应用方法,作用是从第一个联合类型参数 T 中,把第二个联合类型参数 U 出现的联合项去掉。
type Test = Exclude<'A' | 'B', 'A'> // 'B'其实就是应用了分配条件类型:
type Test = Exclude<'A', 'A'> | type Test = Exclude<'B', 'A'>type Test = 'A' extends 'A' ? never : 'A' | 'B' extends 'A' ? never : 'B'
type Test = never | 'B'
type Test = 'B'Extract
type Exclude<T, U> = T extends U ? T : never;Extract 是 TS 内置的应用方法,作用是从第二个联合类型参数 U 中,把第一个联合类型参数 T 出现的联合项提取出来。
type Test = Exclude<'A' | 'B', 'A'> // 'A'其实就是应用了分配条件类型。
type Test = Exclude<'A', 'A'> | type Test = Exclude<'B', 'A'>
type Test = 'A' extends 'A' ? 'A' : never | 'B' extends 'A' ? 'B' : never
type Test = 'A' | never
type Test = 'A'▐ 总结
在 typescript 中的 extends 关键字主要用法,就是继承类型,结合三元表达式来完成更多的类似函数的应用方法,在三元表达式中还要注意分配条件类型的应用。
文章为笔者在学习过程中做的笔记,如果有误,欢迎指正。

团队介绍
我们是大淘宝技术行业与商家技术团队,是消费电子线下业务,主要面向线下门店的分销、经营、零售相关的产品。技术侧属于大淘宝技术前端团队,技术产品服务阿里巴巴整个集团亿万级别的业务。
¤ 拓展阅读 ¤
3DXR技术 | 终端技术 | 音视频技术
服务端技术 | 技术质量 | 数据算法
相关文章:
TypeScript 基础学习之泛型和 extends 关键字
越来越多的团队开始使用 TS 写工程项目, TS 的优缺点也不在此赘述,相信大家都听的很多了。平时对 TS 说了解,仔细思考了解的也不深,借机重新看了 TS 文档,边学习边分享,提升对 TS 的认知的同时,…...
《数据分析-JiMuReport04》JiMuReport报表设计入门介绍-页面优化
报表设计 2 页面优化 如上图所示的报表,仅仅是展示数据,不过这样看起来似乎太草率了,所以再优化一下吧 保存报表后,在积木报表中就可以看到对应的报表文件 此时我们如果还需要编辑报表,就点击这个报表即可 2.1 居中…...
带头双向循环链表及链表总结
1、链表种类大全 1、链表严格来说可能用2*2*28种结构,从是否带头,是否循环,是否双向三个角度区分。 2、无头单向循环链表一般不会在实际运用中直接存储数据,而会作为某些更复杂结构的一个子结构,毕竟它只在头插、头删…...
(八十)MySQL是如何基于各种规则去优化执行计划的?(中)
今天我们来讲一下子查询是如何执行的,以及他的执行计划是如何优化的。比如说类似于下面的SQL语句: select * from t1 where x1 (select x1 from t2 where idxxx) 这就是一个典型的子查询 也就是说上面的SQL语句在执行的时候,其实会被拆分为…...
第一章:命题与命题公式
1.命题与命题联结词 1.命题与命题的表示 1. 命题 由一个或几个已知的前提,推导出来一个未知的结论的思维过程称为推理,推理的基本要素就是表达这些前提的一些陈述句,可以将这些陈述句理解为命题。 (1)地球是行星 (2)8不是素数 (3)1 + 2 = 22. 命题真值 一个陈述句不…...
c/c++开发,无可避免的操作符operator(篇一),操作符重载
一、操作符号重载 虽然c/c内置了大量各类操作符,开发者可以很方便将其应用数学运算、成员访问、类型转换、内存分配等执行语句中,但很多时候,也需要根据项目应用需要,通过操作符重载,能够针对类类型的操作数定义不同的…...
【7.MySQL行格式存储】
1.MySQL数据存放文件 我们每创建一个 database(数据库) 都会在 /var/lib/mysql/ 目录里面创建一个以 database 为名的目录,创建一个student表 [rootxiaodainiao ~]#ls /var/lib/mysql/my_test db.opt student.frm student.ibddb.opt:用…...
【Linux】线程实例 | 简单线程池
今天来写一个简单版本的线程池 1.啥是线程池 池塘,顾名思义,线程池就是一个有很多线程的容器。 我们只需要把任务交到这个线程的池子里面,其就能帮我们多线程执行任务,计算出结果。 与阻塞队列不同的是,线程池中内有…...
ATAC-seq 数据分析实战
文章目录一、 ATAC-seq原理和基础知识1. ATAC-seq原理2. Tn5转座子1. 转座概念2. 参与分子1. 转座子(1) 简化的转座子结构(2) Tn5转座子的结构2. 转座酶3. 转座过程二、数据比对和过滤一、 ATAC-seq原理和基础知识 1. ATAC-seq原…...
设计模式-第13章(状态模式)
状态模式状态模式状态模式的好处和用处工作状态状态模式 状态模式(State),当一个对象的内在状态改变时允许改变其行为,这个对象看起来像是改变了其类。 状态模式主要解决的是当控制一个对象状态转换的条件表达式过于复杂时的情况…...
ReentrantLock源码分析(一)加锁流程分析
一、ReetrantLock的使用示例 static ReentrantLock lock new ReentrantLock(); public static void main(String[] args) throws InterruptedException { new Thread(ClassLayOutTest::reentrantLockDemo, "threadA").start(); Thread.sleep(1000);…...
【C++】list的模拟实现
文章目录1.list 底层2. list的模拟实现1. list_node 类设计2. list类如何调用类型3 .push_back(正常实现)4. 迭代器的实现第一个模板参数Tconst迭代器第二个模板参数Ref第三个模板参数Ptr对list封装的理解5. insert6.push_back与 push_front(复用)7. erase8. pop_back与pop_fro…...
Python连接es笔记三之es更新操作
这一篇笔记介绍如何使用 Python 对数据进行更新操作。 对于 es 的更新的操作,不用到 Search() 方法,而是直接使用 es 的连接加上相应的函数来操作,本篇笔记目录如下: 获取连接update()update_by_query()批量更新UpdateByQuery()…...
哪个牌子的蓝牙耳机音质好?音质比较好的蓝牙耳机排名
蓝牙耳机经过多年发展,无论是在外观设计还是性能配置上都有很大的进步,越来越多的蓝牙耳机开始注重音质表现,逐渐有HIFI音质、无损音质出现在大众视野。那么哪个牌子的蓝牙耳机音质好?接下来,我来给大家分享几款音质比…...
Qt实用技巧:Qt中浮点数的相等比较方式(包括单精度和双精度)
若该文为原创文章,转载请注明原文出处 本文章博客地址:https://hpzwl.blog.csdn.net/article/details/129464152 红胖子(红模仿)的博文大全:开发技术集合(包含Qt实用技术、树莓派、三维、OpenCV、OpenGL、ffmpeg、OSG、单片机、软…...
【数据结构初阶】双向循环链表
目录一.链表的分类二.与单链表相比三.实现增删查改1.双向循环链表结构的创建2.创建新节点3.初始化链表4.头插和尾插5.判断链表是否为空6.头删和尾删7.打印函数8.查找函数9.删除pos位置节点10.在pos前位置插入数据11.优化升级一.链表的分类 链表可有根据单向双向、有无哨兵位、…...
0104BeanDefinition合并和BeanClass加载-Bean生命周期详解-spring
文章目录1 前言2 BeanDefinition合并2.1 BeanDefinition合并在做什么?2.2 BeanDefinition怎么合并2.3 示例演示3 Bean Class 加载后记1 前言 下面要介绍的阶段,都是在调用getBean()从容器中获取bean对象的过程中发生的操作,我们需要更多的去…...
Java集合进阶(三)
文章目录一、Map1. 概述2. 基本功能3. 遍历4. 遍历学生对象5. 集合嵌套6. 统计字符出现次数二、Collections1. 常用方法2. 学生对象排序三、模拟斗地主一、Map 1. 概述 Interface Map<K, V>:K 是键的类型,V 是值的类型。 将键映射到值的对象&…...
【网络】什么是RPC?RPC与HTTP有什么关系?
文章目录RPC是什么RPC和HTTP的关系和区别[附]关于REST论文中提到的"HTTP不是RPC"重点参考 凤凰架构-远程过程调用 既然有HTTP为什么还要有RPC? RPC是什么 RPC(Remote Procedure Call):即远程过程调用,目的是为了让计算机能够跟调用…...
[手撕数据结构]栈的深入学习-java实现
CSDN的各位uu们你们好,今天千泽带来了栈的深入学习,我们会简单的用代码实现一下栈, 接下来让我们一起进入栈的神奇小世界吧!0.速览文章一、栈的定义1. 栈的概念2. 栈的图解二、栈的模拟实现三.栈的经典使用场景-逆波兰表达式总结一、栈的定义 1. 栈的概念 栈:一种…...
在软件开发中正确使用MySQL日期时间类型的深度解析
在日常软件开发场景中,时间信息的存储是底层且核心的需求。从金融交易的精确记账时间、用户操作的行为日志,到供应链系统的物流节点时间戳,时间数据的准确性直接决定业务逻辑的可靠性。MySQL作为主流关系型数据库,其日期时间类型的…...
golang循环变量捕获问题
在 Go 语言中,当在循环中启动协程(goroutine)时,如果在协程闭包中直接引用循环变量,可能会遇到一个常见的陷阱 - 循环变量捕获问题。让我详细解释一下: 问题背景 看这个代码片段: fo…...
FFmpeg 低延迟同屏方案
引言 在实时互动需求激增的当下,无论是在线教育中的师生同屏演示、远程办公的屏幕共享协作,还是游戏直播的画面实时传输,低延迟同屏已成为保障用户体验的核心指标。FFmpeg 作为一款功能强大的多媒体框架,凭借其灵活的编解码、数据…...
Vue3 + Element Plus + TypeScript中el-transfer穿梭框组件使用详解及示例
使用详解 Element Plus 的 el-transfer 组件是一个强大的穿梭框组件,常用于在两个集合之间进行数据转移,如权限分配、数据选择等场景。下面我将详细介绍其用法并提供一个完整示例。 核心特性与用法 基本属性 v-model:绑定右侧列表的值&…...
理解 MCP 工作流:使用 Ollama 和 LangChain 构建本地 MCP 客户端
🌟 什么是 MCP? 模型控制协议 (MCP) 是一种创新的协议,旨在无缝连接 AI 模型与应用程序。 MCP 是一个开源协议,它标准化了我们的 LLM 应用程序连接所需工具和数据源并与之协作的方式。 可以把它想象成你的 AI 模型 和想要使用它…...
【机器视觉】单目测距——运动结构恢复
ps:图是随便找的,为了凑个封面 前言 在前面对光流法进行进一步改进,希望将2D光流推广至3D场景流时,发现2D转3D过程中存在尺度歧义问题,需要补全摄像头拍摄图像中缺失的深度信息,否则解空间不收敛…...
生成 Git SSH 证书
🔑 1. 生成 SSH 密钥对 在终端(Windows 使用 Git Bash,Mac/Linux 使用 Terminal)执行命令: ssh-keygen -t rsa -b 4096 -C "your_emailexample.com" 参数说明: -t rsa&#x…...
涂鸦T5AI手搓语音、emoji、otto机器人从入门到实战
“🤖手搓TuyaAI语音指令 😍秒变表情包大师,让萌系Otto机器人🔥玩出智能新花样!开整!” 🤖 Otto机器人 → 直接点明主体 手搓TuyaAI语音 → 强调 自主编程/自定义 语音控制(TuyaAI…...
JUC笔记(上)-复习 涉及死锁 volatile synchronized CAS 原子操作
一、上下文切换 即使单核CPU也可以进行多线程执行代码,CPU会给每个线程分配CPU时间片来实现这个机制。时间片非常短,所以CPU会不断地切换线程执行,从而让我们感觉多个线程是同时执行的。时间片一般是十几毫秒(ms)。通过时间片分配算法执行。…...
2025季度云服务器排行榜
在全球云服务器市场,各厂商的排名和地位并非一成不变,而是由其独特的优势、战略布局和市场适应性共同决定的。以下是根据2025年市场趋势,对主要云服务器厂商在排行榜中占据重要位置的原因和优势进行深度分析: 一、全球“三巨头”…...
