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

news2024/11/26 3:40:42

b8fd221fb5967b382ea0fac973918c5a.gif

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

026813f6813fd567bbe8ba5c14d8d0f2.png

泛型

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 为 string


print('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 就像一个占位符、或者说一个变量,在使用的时候可以把定义的类型像参数一样传入,它可以原封不动地输出。

8d13bb66decf36ff3af49425db89849a.png

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: string
  age: 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; // number


type P<T> = T extends 'x' ? string : number;
type Test3 = P<'x' | 'y'> // tring | number


type 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; // string


type 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 关键字主要用法,就是继承类型,结合三元表达式来完成更多的类似函数的应用方法,在三元表达式中还要注意分配条件类型的应用。

文章为笔者在学习过程中做的笔记,如果有误,欢迎指正。

2c00ebab2d61f4faab500dcbfe6056f7.png

团队介绍

我们是大淘宝技术行业与商家技术团队,是消费电子线下业务,主要面向线下门店的分销、经营、零售相关的产品。技术侧属于大淘宝技术前端团队,技术产品服务阿里巴巴整个集团亿万级别的业务。

¤ 拓展阅读 ¤

3DXR技术 | 终端技术 | 音视频技术

服务端技术 | 技术质量 | 数据算法

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

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

相关文章

Qt静态扫描(命令行操作)

Qt静态扫描&#xff08;命令行操作&#xff09; 前沿&#xff1a; 静态代码分析是指无需运行被测代码&#xff0c;通过词法分析、语法分析、控制流、数据流分析等技术对程序代码进行扫描&#xff0c;找出代码隐藏的错误和缺陷&#xff0c;如参数不匹配&#xff0c;有歧义的嵌…

Linux查看UTC时间

先了解一下几个时间概念。 GMT时间&#xff1a;Greenwich Mean Time&#xff0c;格林尼治平时&#xff0c;又称格林尼治平均时间或格林尼治标准时间。是指位于英国伦敦郊区的皇家格林尼治天文台的标准时间。 GMT时间存在较大误差&#xff0c;因此不再被作为标准时间使用。现在…

数据传输服务DTS的应用场景(阿里巴巴)

数据传输服务DTS的应用场景(阿里巴巴) 数据传输服务DTS&#xff08;Data Transmission Service&#xff09;支持数据迁移、数据订阅和数据实时同步功能&#xff0c;帮助您实现多种典型应用场景。 不停机迁移数据库 传输方式&#xff1a;数据迁移 为了保证数据的一致性&#…

【17】组合逻辑 - VL17/VL19/VL20 用3-8译码器 或 4选1多路选择器 实现逻辑函数

VL17 用3-8译码器实现全减器 【本题我的也是绝境】 因为把握到了题目的本质要求【用3-8译码器】来实现全减器。 其实我对全减器也是不大清楚,但是仿照对全加器的理解,全减器就是低位不够减来自低位的借位 和 本单元位不够减向后面一位索要的借位。如此而已,也没有很难理解…

Python3简单实现图像风格迁移

导语T_T之前似乎发过类似的文章&#xff0c;那时候是用Keras实现的&#xff0c;现在用的PyTorch&#xff0c;而且那时候发的内容感觉有些水&#xff0c;于是我决定。。。好吧我确实只是为了写点PyTorch练手然后顺便过来水一篇美文~~~利用Python实现图像风格的迁移&#xff01;&…

Python实现性能测试(locust)

一、安装locustpip install locust -- 安装&#xff08;在pycharm里面安装或cmd命令行安装都可&#xff09;locust -V -- 查看版本&#xff0c;显示了就证明安装成功了或者直接在Pycharm中安装locust:搜索locust并点击安装&#xff0c;其他的第三方包也可以通过这种方式二、loc…

JavaScript Math(算数)对象

Math&#xff08;算数&#xff09;对象的作用是&#xff1a;执行常见的算数任务。在线实例round()如何使用 round()。random()如何使用 random() 来返回 0 到 1 之间的随机数。max()如何使用 max() 来返回两个给定的数中的较大的数。&#xff08;在 ECMASCript v3 之前&#xf…

站外seo优化有用吗?值得投入时间和精力吗?

随着互联网的普及和竞争的激烈化&#xff0c;SEO&#xff08;Search Engine Optimization&#xff0c;搜索引擎优化&#xff09;已经成为各种网站推广的必备技能。 而站外SEO优化就是指通过在其他网站上增加链接和引用等方式&#xff0c;来提高自己网站的搜索引擎排名和曝光度…

【6G 新技术】6G数据面介绍

博主未授权任何人或组织机构转载博主任何原创文章&#xff0c;感谢各位对原创的支持&#xff01; 博主链接 本人就职于国际知名终端厂商&#xff0c;负责modem芯片研发。 在5G早期负责终端数据业务层、核心网相关的开发工作&#xff0c;目前牵头6G算力网络技术标准研究。 博客…

window.onresize的详细使用

最近做的项目老是涉及到大小屏切换&#xff0c;但是因为屏幕宽高不一样的原因&#xff0c;老是要计算表格高度 window.onresize&#xff1a;监听window窗口变化&#xff0c;当窗口大小发生变化时&#xff0c;会触发此事件 含义 MDN中的定义是这样子的&#xff1a; 文档视图调…

GitHub与PicGo搭建免费稳定图床并实现Typora内复制自动上传

本文介绍基于Github平台与PicGo工具&#xff0c;构建免费、稳定的图床&#xff0c;并实现在Typora内撰写Markdown文档时&#xff0c;粘贴图片就可以将这一图片自动上传到搭建好的图床中的方法。 1 配置GitHub 首先&#xff0c;我们需要配置Github&#xff0c;创建一个仓库从而…

mysql 查询一个表的数据,并修改部分数据,再插回原来的表中,复制某个用户的数据给另一个用户

mysql 查询一个表的数据&#xff0c;并修改部分数据&#xff0c;再插回原来的表中&#xff0c;复制某个用户的数据给另一个用户 一、需求 我有一表日记的表&#xff0c;表中盛放着所有用户的日记数据。 在做演示项目的时候&#xff0c;我需要将一个用户的数据复制给另一个用户…

PlotNeuralNet + ChatGPT创建专业的神经网络的可视化图形

PlotNeuralNet&#xff1a;可以创建任何神经网络的可视化图表&#xff0c;并且这个LaTeX包有Python接口&#xff0c;我们可以方便的调用。 但是他的最大问题是需要我们手动的编写网络的结构&#xff0c;这是一个很麻烦的事情&#xff0c;这时 ChatGPT 就出来了&#xff0c;它可…

JavaScript学习笔记(3.0)

数组是一种特殊类型的对象。在JavaScript中对数组使用typeof运算符会返回“object”。 但是&#xff0c;JavaScript数组最好以数组来描述。 数组使用数字来访问其“元素”。比如person[0]访问person数组中的第一个元素。 <!DOCTYPE html> <html> <body>&l…

【JavaEE进阶】——第一节.Maven国内源配置

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 文章目录 前言 操作步骤 1.打开项目配置界面&#xff08;当前项目配置&#xff09; 2.检查并配置国内源 3.再次打开项目配置界面&#xff08;新项目配置&#xff09; 4…

Android RecyclerView的notify方法和动画的刷新详解

前些天发现了一个蛮有意思的人工智能学习网站,8个字形容一下"通俗易懂&#xff0c;风趣幽默"&#xff0c;感觉非常有意思,忍不住分享一下给大家。 &#x1f449;点击跳转到教程 前言&#xff1a; 本篇讲解了RecyclerView关于通知列表刷新的常用的notify方法。和Recy…

综合练习7 摄氏度转华氏温度(“\t“的使用,循环语句)

综合练习7 摄氏度转华氏温度 使用do…while循环&#xff0c;在控制台输入摄氏温度与华氏温度的对照表。 对照表从摄氏温度-30℃到50℃&#xff0c;每行间隔10℃&#xff0c;运行如下&#xff1a; 摄氏温度&#xff1a;-30℃ 华氏温度&#xff1a;-22.0℉ 摄氏温度&#xff1a;…

【专项训练】动态规划-3

动态规划:状态转移方程、找重复性和最优子结构 分治 + 记忆化搜索,可以过度到动态规划(动态递推) function DP():# DP状态定义# 需要经验,需把现实问题定义为一个数组,一维、二维、三维……dp =[][] # 二维情况for i = 0...M:

自动化测试的定位及一些思考

大家对自动化的理解&#xff0c;首先是想到Web UI自动化&#xff0c;这就为什么我一说自动化&#xff0c;公司一般就会有很多人反对&#xff0c;因为自动化的成本实在太高了&#xff0c;其实自动化是分为三个层面的&#xff08;UI层自动化、接口自动化、单元测试&#xff09;&a…

井字棋--课后程序(Python程序开发案例教程-黑马程序员编著-第7章-课后作业)

实例2&#xff1a;井字棋 井字棋是一种在3 * 3格子上进行的连珠游戏&#xff0c;又称井字游戏。井字棋的游戏有两名玩家&#xff0c;其中一个玩家画圈&#xff0c;另一个玩家画叉&#xff0c;轮流在3 * 3格子上画上自己的符号&#xff0c;最先在横向、纵向、或斜线方向连成一条…