typescript 类型运算探讨

news2025/1/16 1:03:15

以函数的方式来看typescript 类型运算

  • 尖括号 <>
  • TypeScript 类型运算符
    • 1、extends
      • 2、keyof
      • 3、infer
      • 4、in
    • 题目示例

对于初接触typescript的前端开发者来说,我们可能会对typescript的类型运算感到很陌生和难以理解。现在想说的是,其实我们可以换另外一种方式来看这个问题,或许会感觉不一样。那就是可以尝试使用我们熟悉的函数来辅助理解。

尖括号 <>

如果用函数来做对比,尖括号<>可以看做是函数的小括号(),<a,b>,我们可以和函数的(a,b)做对比,也就是两个参数

TypeScript 类型运算符

当然,typescript类型运算毕竟不是javascript的函数,在这之前,我们得先对其运算符有个了解

1、extends

extends , 我们可以拿他来和javascript的instanceof 来做对比理解

比如 声明一个变量 var arr ,要判断arr是不是数组,可以使用 arr instanceof Array

同理,假设有个类型 TestType

type TestType = {
  name:string,
  age:number
}

想判断某个类型是不是 TestType类型,我们可以如下使用

type TestType = {
  name:string,
  age?:number
}

type testType1 = 1
type testType2 = {name:string}
type testType3 = {name:string, age:number}
type testType4 = {name:string, age:number, isChild: boolean}
type testType5 = { age:number}

type res1 = testType1 extends TestType ? number|string : boolean  // boolean  
type res2 = testType2 extends TestType ? number|string : boolean  //  number|string  
type res3 = testType3 extends TestType ? number|string : boolean  // number|string
type res4 = testType4 extends TestType ? number|string : boolean  //  number|string
type res5 = testType5 extends TestType ? number|string : boolean  // boolean  

小技巧:使用编辑器的提示可以直接看运算结果
在这里插入图片描述

2、keyof

获取 一个 类型的所有 key 的并返回生成的联合类型,类似javascriptObject.keys获取对象的键名一样

type TestType = {
  name:string,
  age?:number
}
interface TestKeyofType {
  name: string,
  value: number
}
type T1 = keyof TestType // "name" | "age"
type T2= keyof TestKeyofType // "name" | "value"

3、infer

用来在extends语句中,在true 的条件分支中,推断一个类型的变量

// 如果泛型T是{ name: infer R, age?: infer R }的子集,则返回infer R获取到的类型,否则返回boolean
// infer R 相当于推断出R的类型
type Func<T> = T extends { name: infer R, age?: infer R } ? R : boolean
type Func2<T> = T extends ()=> infer R ? R : boolean
type Func3<T> = T extends infer R ? R : boolean

let func1: Func<number> // boolean;
let func2: Func<""> // boolean
let func3: Func<() => Promise<number>> // boolean
let func4: Func<{ name:string, age:number, value: boolean }> // string|number

4、in

in 用来遍历类型的key,可以和for in 遍历对象做对比

题目示例

1、实现一个IsEqual工具类型,用于比较两个类型是否相等。具体的使用示例如下所示

type IsEqual<A, B> = // 你的实现代码

// 测试用例
type E0 = IsEqual<1, 2>; // false
type E1 = IsEqual<{ a: 1 }, { a: 1 }> // true
type E2 = IsEqual<[1], []>; // false

分析:如果这是一个函数,那就变成:实现一个IsEqual工具函数,用于比较两个值是否相等。实现

function isEqual(a, b) {
  // return a === b ? true : false
  return a === b
}

换成类型运算,那就判断a,b两个类型是否相等。使用extends

type IsEqual<A, B> = A extends B ? B extends A ? true : false : false
type IsEqual2<A, B> = [A, B] extends [B, A] ? true : false
type result = IsEqual<2, 3>
type result2 = IsEqual<3, 3>
type result3 = IsEqual<2, 3>
type result4 = IsEqual<3, 3>

2、 实现一个UnionToIntersection工具类型,用于把联合类型转换为交叉类型。具体的使用示例如下所示:

type UnionToIntersection<U> = // 你的实现代码

// 测试用例
type U0 = UnionToIntersection<string | number> // never
type U1 = UnionToIntersection<{ name: string } | { age: number }> // { name: string; } & { age: number; }

首先看下联合类型:即** string | number | boolean | {name:string,value:boolean}** 用 “|”,来连接的类型,可以看着取类型并集
然后看下交叉类型:及** string & number & boolean & {name:string,value:boolean}**用 “&”,来连接的类型,取类型交集
这个用函数比喻貌似进行不下去了。

直接先看下参考答案吧

export type UnionToIntersection<Union> = (
  Union extends unknown ? (distributedUnion: Union) => void : never
) extends (mergedIntersection: infer Intersection) => void
  ? Intersection
  : never;

这里涉及一个重要的知识点,逆变与协变。关于这个,本人目前理解也不透彻`,参考文章:https://zhuanlan.zhihu.com/p/454202284

尽管不能对逆变和协变做深入剖析,但是我们仍然先要记住下面两点:使用infer推导类型,推导值时会返回联合类型,推导参数时会返回交叉类型

type Bar<T> = T extends { a: (x: infer U) => void, b: (x: infer U) => void } ? U : never
type T20 = Bar<{ a: (x: string) => void, b: (x: string) => void }> // string
type T21 = Bar<{ a: (x: string) => void, b: (x: number) => void }> // string & number

type Bar2<T> = T extends { a: infer U, b: infer U } ? U : never
type T201 = Bar2<{ a: string, b: string }> // string
type T211 = Bar2<{ a: string, b: number }> // string | number

这里剩下的疑问是 Union extends unknown 的作用。疑惑点在于 Union extends unknown 永远是true,但是我们如果换其他形式条件,结果却不一样。如下

export type UnionToIntersection<Union> = (Union extends unknown ? (distributedUnion: Union) => void : false) extends (mergedIntersection: infer Intersection) => void ? Intersection : false
export type UnionToIntersection2<Union> = ((distributedUnion: Union) => void) extends (mergedIntersection: infer Intersection) => void ? Intersection : false
export type UnionToIntersection3<Union> = (Union extends string ? (distributedUnion: Union) => void : false) extends (mergedIntersection: infer Intersection) => void ? Intersection : false
export type UnionToIntersection4<Union> = (1 extends number ? (distributedUnion: Union) => void : false) extends (mergedIntersection: infer Intersection) => void ? Intersection : false

type U0 = UnionToIntersection<string | number> // never
type U1 = UnionToIntersection<{ name: string } | { age: number }> // { name: string; } & { age: number; }

type U02 = UnionToIntersection2<string | number> // string | number
type U12 = UnionToIntersection2<{ name: string } | { age: number }> // { name: string; } & { age: number; }

type U03 = UnionToIntersection3<string | number> // false
type U13 = UnionToIntersection3<{ name: string } | { age: number }> // false

type U04 = UnionToIntersection4<string | number> // string | number
type U14 = UnionToIntersection4<{ name: string } | { age: number }> // { name: string; } & { age: number; }

type Test = (string | number) extends unknown ? true : false
type Tes3 = 1 extends number ? true : false
type Test2 = ({ name: string } | { age: number }) extends unknown ? true : false

Union extends unknown 的作用的左右需待后续查明

3 实现一个Merge工具类型,用于把两个类型合并成一个新的类型。第二种类型(SecondType)的Keys将会覆盖第一种类型(FirstType)的Keys。具体的使用示例如下所示:

type Foo = { 
   a: number;
   b: string;
};

type Bar = {
   b: number;
};

type Merge<FirstType, SecondType> = // 你的实现代码

const ab: Merge<Foo, Bar> = { a: 1, b: 2 };

这里我们倒是可以用对象的合并来类比一下,假设有如下对象

var foo = {
 a:"number",
 b:"string"
}
var bar = {
 b:"number"
}

做合并如下:

var mergeObject = {
...foo,
...bar
} // {a:"number",b:"number"}

对于typescript来说,要实现合并,首先可以使用的基础方法就是交叉。交叉可以取到所有的类型键名,即Foo&Bar可以得到含有a,b键名的类型
第二、如果直接使用交叉Foo&Bar,我们得到的结果如下

type Foo = { 
   a: number;
   b: string;
};

type Bar = {
   b: number;
};
type MergeType = Foo & Bar

这样得到的MergeType类型和如下一致

type MergeType2 = {
a:number,
b:string&number
}

a没问题,是我们想要的,b由于即使string又是number,最终只会得到never。而我们的要求是后面覆盖前面,即b应该是number

第三、剩下的那也好理解了,遍历Foo&Bar的交叉类型,使用keyof 取得键名,先判断Bar里面有没有,有就取 Bar的值,没有就判断Foo有没有,有就取Foo的值

最终结果

type Merge<FirstType, SecondType> = {
  [K in keyof (FirstType & SecondType)]: K extends keyof SecondType
  ? SecondType[K]
  : K extends keyof FirstType
  ? FirstType[K]
  : never;
};

4 实现一个RemoveIndexSignature工具类型,用于移除已有类型中的索引签名。具体的使用示例如下所示:

interface Foo {
  [key: string]: any;
  [key: number]: any;
  bar(): void;
}

type RemoveIndexSignature<T> = // 你的实现代码

type FooWithOnlyBar = RemoveIndexSignature<Foo>; //{ bar: () => void; }

索引签名:用来解决无法预估的多余数据的一种方案,例如[k:string]:any
这个如果和对象类比,就是删除对应的属性
由于属性名未知,要删除我们就要用遍历,typescript中返回never就表示没有,也可以理解为被删除
使用 keyof获得键名的联合类型、用in遍历联合类型, 使用extends判断是否是字符串或者数字,是返回相应的类型,不是返回never

type RemoveIndexSignature<T> = {
  [K in (keyof T) as number extends K ? never : string extends K ? never : K]: T[K];
};

使用as强转的原因,是最后面的:k报错:TS2313: Type parameter ‘k’ has a circular constraint.。如果改成如下

type keyType2<T> = {
  [k in (keyof T) extends k ? never : "k"]: any
}

则不会报错。关于报 Type parameter ‘k’ has a circular constraint 的具体原因目前没查到,未知

5 实现RequireAllOrNone工具类型,用于满足以下功能。即当设置age属性时,gender属性也会变成必填。具体的使用示例如下所示:

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

type RequireAllOrNone<T, K extends keyof T> = // 你的实现代码

const p1: RequireAllOrNone<Person, 'age' | 'gender'> = {
  name: "lolo"
};

const p2: RequireAllOrNone<Person, 'age' | 'gender'> = {
  name: "lolo",
  age: 7,
  gender: 1
};

参考答案如下:

type RequireAllOrNone<T, K extends keyof T> = Omit<T, K> & (Required<Pick<T, K>> | Partial<Record<K, never>>)

1、了解工具函数 Omit,接收两个参数,第一个是要操作的类型,第二个是要删除的属性。可以理解为:删除某个类型的某个属性
假设这是一个对象删除属性的工具函数,那我们可如下实现

// pramNames: "a"|"b"|"c"
function omit(obj, pramNames){
  const pramNamesArr = pramNames.split("|")
  for(let i in obj){
    if(pramNamesArr.includes(i)){
      delete obj[i]
    }
  }
}

2、Pick:接收两个参数,第一个是要操作的类型,第二个是要提取出来的属性。可以理解为将第一个参数的部分类型(由第二个参数指定)的属性取出来,返回只含有这部分属性的一个新类型。
同样,假设这是一个js的工具函数,我们也可以很快就实现这个工具

// pramNames: "a"|"b"|"c"
function pick(obj, pramNames){
  const pramNamesArr = pramNames.split("|")
  const newObj = {}
  for(let i in obj){
    if(pramNamesArr.includes(i)){
      newObj[i] = obj[i]
    }
  }
  return newObj
}

3、Required 可选转必选
4、Partial 必选转可选
5、Record 接收两个参数,第一个参数是类型,第二个参数是要转化的类型值。作用就是将第一个参数的所有类型值转化成第二个参数指定的类型值

我们来看第一步:Omit<T, K> ,先将要处理的类型全部删除

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

type RequireAllOrNone<T, K extends keyof T> = Omit<T, K>

const p1: RequireAllOrNone<Person, "age" | "gender"> = {
  name: "lolo",
}
const p1: RequireAllOrNone<Person, "age" | "gender"> = {
  name: "lolo",
  age: 1
}

这段代码p1正常,p2报错。英文age,gender属性已经被删除
得到的类型结果是

interface Person1 {
 name: string;
}

第二步:Required<Pick<T, K>>

interface Person {
  name: string;
  age?: number;
  gender?: number;
}
type RequireAllOrNone2<T, K extends keyof T> = Required<Pick<T, K>>

const p3: RequireAllOrNone2<Person, "age" | "gender"> = {
  name: "lolo",
  age: 1
}
const p4: RequireAllOrNone2<Person, "age" | "gender"> = {
  age: 1,
  gender: 18
}

上面是将传进来的"age"和"gender"挑出来做一个新的类型,且全部为必选。所以最终的类型没有name了
得到的类型结果是

interface Person2 {
age: number;
 gender: number;
}

看 第三步 , Partial<Record<K, never>>

interface Person {
  name: string;
  age?: number;
  gender?: number;
}
type RequireAllOrNone3<T, K extends keyof T> = Partial<Record<K, never>>

const p5: RequireAllOrNone3<Person, "age" | "gender"> = {
  name: "lolo",
  age: 1
}
const p6: RequireAllOrNone3<Person, "age" | "gender"> = {
  age: undefined,
  gender: undefined
}
const p7: RequireAllOrNone3<Person, "age" | "gender"> = {
  name: "lolo",
  age: undefined,
  gender: undefined
}

上面p5、p7报错,p6正常,因为最终得出的类型是

interface Person3 {
 age?: undefined;
 gender?: undefined;
}

最后组合一块

type Persons = Person1 & (Person2 | Person3)

这里实现有age就有gender,用到的是联合类型的匹配逻辑,及如果age给到数字,就好自动匹配到Person2的类型,这时候就要求有gender,如果没有age,那就是undefinde,自动匹配Person3

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

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

相关文章

我国服务行业体经济尽显强大韧性 2021年全年总体保持恢复性增长态势

根据观研报告网发布的《2022年中国服务行业分析报告-行业发展监测与投资潜力分析》显示&#xff0c;所谓服务业&#xff0c;不是指商品的买卖&#xff0c;而是指通过各种不同的服务工作&#xff0c;让顾客得到满足。 服务业有传统服务业与现代服务业之分。传统服务业是指为人们…

【云原生进阶之容器】第一章Docker核心技术1.7节——Docker镜像技术剖析

1 容器镜像概述 1.1 什么是镜像 镜像就是一个可执行独立运行的软件包。包含应用运行所必须的文件和依赖包;镜像可以理解为类或者模板,只要在容器的环境下开箱即用; Docker容器与镜像的关系: 1.2 bootfs和rootfs 通常而言,Linux的操作系统由两类文件系统组成:bootfs…

一、Kubernetes基本介绍和功能架构

1、kubernetes 概述 kubernetes&#xff0c;简称 K8s&#xff0c;是用 8 代替 8 个字符“ubernete”而成的缩写。是一个开源 的&#xff0c;用于管理云平台中多个主机上的容器化的应用&#xff0c;Kubernetes 的目标是让部署容器化的 应用简单并且高效(powerful),Kubernetes 提…

python中的异常捕获与传递

目录 一.了解异常 二.异常的捕获方法 1.捕获常规异常 演示 2.捕获制定异常 演示 3.捕获多个异常 演示 4.捕获所有异常 演示 5.异常else 演示 6.异常的finally 演示 三.异常的传递 一.了解异常 当我们的程序遇到了BUG&#xff0c;那么接下来有两种情况: 整个程序因…

leetcode.1971 寻找图中是否存在路径 - bfs dfs 并查集 邻接表 邻接矩阵

1971. 寻找图中是否存在路径 目录 1、bfs - 邻接矩阵 2、dfs - 邻接表 3、并查集 1、bfs - 邻接矩阵 让源点source入队 取队头遍历未标记的邻接节点并入队如果队伍里有dest目标节点 说明dest被遍历到 则return trueclass Solution { public:static const int N2*1e5;bool st[…

靴子落地:ChatGPT 国内发展或被「拉手刹」

内容一览&#xff1a;深度合成服务在满足用户需求、改进用户体验的同时&#xff0c;也被一些不法人员用于制作、复制、发布、传播违法信息&#xff0c;诋毁、贬损他人名誉、荣誉&#xff0c;仿冒他人身份实施诈骗等违法行为&#xff0c;如今针对这一技术的管理规定终于发布了。…

游戏开发 mask和RectMask2D区别

1. Mask遮罩的大小与形状依赖于Graphic&#xff0c;而RectMask2D只需要依赖RectTransform 2. Mask支持圆形或其他形状遮罩&#xff0c; 而RectMask2D只支持矩形 3. Mask会增加drawcall 4、mask的性质&#xff1a; 性质1&#xff1a;Mask会在首尾&#xff08;首Mask节点&…

Jenkins(4)— 配置钉钉通知

1、创建钉钉机器人 自定义机器人接入 - 钉钉开放平台 群设置 >> 智能群助手 >> 添加机器人 >> 自定义webhook机器人 输入自定义钉钉机器人名字和选择必要的安全设置。 注意&#xff1a;复制上面的加密信息和Webhook&#xff0c;后面在jenkins配置钉钉机器…

[附源码]计算机毕业设计Python的家政服务平台(程序+源码+LW文档)

该项目含有源码、文档、程序、数据库、配套开发软件、软件安装教程 项目运行 环境配置&#xff1a; Pychram社区版 python3.7.7 Mysql5.7 HBuilderXlist pipNavicat11Djangonodejs。 项目技术&#xff1a; django python Vue 等等组成&#xff0c;B/S模式 pychram管理等…

解决jeecgboot框架下单点登录后,页面刷新后,会自动跳转到单点登录服务的登录页问题

1.配置单点服务&#xff0c;可参考官方文档&#xff1a;http://doc.jeecg.com/2044170 - 打开.env文件,UE_APP_SSOtrue即代表开启SSO登录 NODE_ENVproduction VUE_APP_PLATFORM_NAMEJeecg-Boot 企业级快速开发平台 VUE_APP_SSOtrue- 打开.env.development文件&#xff0c;修改…

黑*头条_第7章_app端文章搜索(新版)

黑*头条_第7章_app端文章搜索(新版) 文章目录黑*头条_第7章_app端文章搜索(新版)1) 今日内容介绍1.1)App端搜索-效果图1.2)今日内容2) 搭建ElasticSearch环境2.1) 拉取镜像2.2) 创建容器2.3) 配置中文分词器 ik2.4) 使用postman测试3) app端文章搜索3.1) 需求分析3.2) 思路分析…

m基于CNN卷积神经网络的IBDFE单载波频域均衡算法

目录 1.算法描述 2.仿真效果预览 3.MATLAB核心程序 4.完整MATLAB 1.算法描述 单载波频域均衡(SC-FDE)是解决符号间干扰(ISI)问题的一项重要技术。相比于单载波时域均衡(SC-TDE)技术和正交频分复用(OFDM)技术,SC-FDE技术具有复杂度低、峰均功率比小的优点。但是,SC-FDE技术中…

Android 测试文字编码格式

测试文字编码格式&#xff0c;与设置字符串格式 调用&#xff1a; juniversalchardet-1.0.3.jar app里的Build.gradle implementation files(libs\\juniversalchardet-1.0.3.jar) java调用&#xff1a; import org.mozilla.universalchardet.UniversalDetector;/*** 测试编…

面试:分库分表经典15连问

目录 1. 我们为什么需要分库分表 1.1 为什么要分库 1.2 为什么要分表 2. 什么时候考虑分库分表&#xff1f; 3. 如何选择分表键 4.非分表键如何查询 5. 分表策略如何选择 5.1 range范围 5.2 hash取模 5.3 一致性Hash 6. 如何避免热点问题数据倾斜&#xff08;热点数…

LaTex教程(二)——LaTeX排版

文章目录1. 中文宏包2. 字符2.1 空格和分段2.2 标点符号2.2.1 引号2.2.2 连字号和破折号2.2.3 省略号3. 文字强调4. 断行断页1. 中文宏包 ctex 宏包和文档类是对CJK 和xeCJK 等宏包的进一步封装。ctex 文档类包括 ctexart /ctexrep / ctexbook&#xff0c;是对LATEX 的三个标准…

第十七届D2大会(II)

一、无极&#xff1a;面向复杂B端项目的低代码平台设计与实践 页面片&#xff1a;自定义的最小低代码开发单元&#xff0c;包括&#xff1a;数据、布局、业务逻辑等低代码编辑器可配合配置分支管理、DevTool等能力&#xff0c;提供更好的工程体验 二、基于H5页面“高差指纹”技…

湿气是怎么来的?身体湿气重什么症状

相信大家在生活中都听说过湿气这个词&#xff0c;尤其是老一辈的人经常会在夏季说晚上湿气重之类的话。其实&#xff0c;从中医的角度来看&#xff0c;湿气更多的是指内脏器官功能失调引起的内生湿邪&#xff0c;主要与脾脏有关。 中医认为&#xff0c;脾胃具有运输水分和湿度的…

Python:python镜像源管理

文章目录常用镜像源配置镜像源&#xff08;1&#xff09;pycharm内部配置&#xff08;2&#xff09;手动添加镜像源&#xff0c;临时使用&#xff08;3) 永久配置镜像源&#xff0c;设置默认anaconda小结常用镜像源 清华&#xff1a;https://pypi.tuna.tsinghua.edu.cn/simple…

JAVA中Volatile/Synchronized

线程安全问题的发生&#xff1a; java的线程内存模型中定义了每个线程都有一份自己的共享变量副本&#xff08;本地内存&#xff09;&#xff0c;里面存放自己私有的数据&#xff0c;其他线程不能直接访问&#xff0c;而一些共享变量则存在主内存中&#xff0c;供所有线程访问…

DBCO-Sulfo-NHS二苯基环辛炔-磺基活性酯1400191-52-7水溶性试剂

DBCO-Sulfo-NHS Ester二苯基环辛炔-磺基活性酯1400191-52-7 名称&#xff1a;二苯基环辛炔-磺基-琥珀酰亚胺酯 英文名称&#xff1a;DBCO-Sulfo-NHS Ester 结构式&#xff1a; CAS:1400191-52-7 外观&#xff1a;固体/粉末 分子式&#xff1a;C25H21N2NaO8S 分子量&#x…