深入了解TypeScript中的类型操作符
前言
本文主要整理介绍关于
Typescripe
中对于类型的操作,从而创建出新的类型,也就是说无需从头到位来编写一类型,而是基于原始类型,借助于相关的类型操作符
,以及组合其他的类型工具
,来创建新的一类型!
泛型Generics
泛型是一种类型变量的概念,可以帮助我们来编写更加灵活更加通用的代码, 😕 既然是“变量”,那么在定义的时候可以通过“传参”的方式来使用,只是这个参数它是一个类型参数,通过在需要的时候,往方法、接口、类中传递类型,即可知道当前的方法、接口、类即将作用在哪种类型上!
1 | // 泛型函数 |
范型约束
有两个以下的泛型函数:
1 | function firstElement1<Type>(arr: Type[]) { |
⭐ 上述两个函数在定义上有一个细微的差别:firstElement1
函数使用了一个泛型类型Type
,表示函数可以接收任何类型的数组作为参数,并返回数组的第一个元素,函数参数arr
是一个由泛型Type
所组成的数组,而firstElement2
函数则使用了一个**泛型约束extends any[]
**,表示Type
必须是一个数组,函数参数arr
的类型是泛型Type
,它可以是任何满足约束的数组类型,两者在使用上,几乎是一致的,都是获取数组的第一个元素,然而它们在类型推断和使用方式上有小小的区别:
- 对于
firstElement1
函数,由于参数arr
是一个泛型数组类型Type[]
,因此在调用时无需显示地指定泛型类型,Typescript
可以根据传递的数组类型自动推断出泛型类型,如下示例所示:1
2const result1 = firstElement1([1, 2, 3]);// result1类型是number
const result2 = firstElement1(['a', 'b', 'c']); //result2类型是string - 对于
firstElement2
函数,参数arr
是一个范型Type
,它是一个可以任何满足约束的数组类型,由于这里泛型类型Type
并没有指定任何具体的类型(Type extends any[]
),因此,在调用的时候需要显示地制定泛型类型,如下代码所示:1
2const result1 = firstElement2<number>([1, 2, 3]); // result1类型是number
const result2 = firstElement2<string>(['a', 'b', 'c']); // result2类型是string
:t-rex: 泛型约束是由Typescript
中用来限制泛型类型参数的一种机制,当我们在使用泛型的时候,有时候希望泛型参数拥有特定的属性或者满足某些条件,而不是任意的类型,泛型约束允许我们指定泛型参数必须满足的条件,从而来提供更多的类型安全性
,一般情况下,泛型约束使用extends
关键词来定义,通过在泛型参数后面使用extends
关键词,我们可以限制泛型参数必须是某种特定的类型或者符合特定条件!
那么,什么是泛型类型参数呢?也就是在声明泛型参数方法、接口、类的时候,其中的
T、K、Type
等等,这些是类型参数,是变量,既然是变量,那么就可以像函数参数的默认值一样,通过“=”来赋值类型的默认值的方式,而且,如果有多个类型参数时,从左往右,左边的类型参数又可以作为右边的类型参数的值来进行逻辑运算,这听着有点绕,具体见 👇 的例子:
1 | declare function create<T extends HTMLElement = HTMLDivElement, U = T[]>(element?: T, children: U[]) |
keyof操作符
keyof
运算符接收一个对象类型,并生成该对象中的key所组成的字符串或者数字字面量联合对象
1 | type Point = {x: number, y: number} |
如果所作用的对象类型是一个索引访问类型的对象的话,那么
keyof
将会返回如下的类型:
1 | type Arrayish = { [n: number]: unknown } |
👉 在
Typescript
中,keyof
是一种类型操作符,用于获取类型中所有键(属性名)的联合类型,返回一个字符串或者数字类型的联合类型,表示指定类型的所有可访问键,使用keyof
操作符可以让我们在编译时获取某个类型的所有属性名,然后在代码中使用这些属性名来返回对象的属性或者定义更加灵活的函数和类型,比如有 👇 的一个例子
1 | interface Person { |
⭐ 上述我们定义了一个函数getProperty
,用于从某个对象中获取属性对应的值,如果属性名不存在于对象中的话,将在编译时报错
typeof操作符
javascript
中本来就有这个typeof
关键词,用于获取一个变量的类型,而在Typescript
中,则使用typeof
来引用变量或者属性的类型,可以获取一个值的类型而不需要实际运行代码!
单纯的将typeof
作用于基本数据类型好像没多大用处,但是如果与其他类型运算符结合起来,可以方便地表达许多的模式,比如有下面的一个例子:
1 | type Predicate = (x: unknown) => boolean |
在Typescript
中如果想要定义一个新的类型,不能直接将一个变量的值直接赋予类型,而必须要使用typeof
关键词
索引访问类型
👇 有下面的这样的一个例子
1 | type Person = { age: number, name: string, alive: boolean } |
🌝 从上述的代码中可以看出,在Typescript
中可以通过使用[key]
对一个类型对象,从一个类型对象取一个/多个key
来作为新的类型,一般是通过索引访问的方式来组成一个新的联合类型!
🤔 如果被作用的对象是一个数组对象的话,那么可以采用 👇 的方式来获取数组对象成员的类型
1 | const MyArray = [ |
条件类型
1 | interface Animal {} |
⭐ 当左侧的类型extends
可分配给右侧的类型时,将获得true
分支逻辑,也就是number类型,否则将获得false
分支逻辑,也就是string类型,这看起来有点类似于javascript中的“三目运算符”。
😕 那么这个这种分支类型有什么用途呢?在了解关于这种分支类型的用途之前,先来了解一下Typescript
中的重载!
函数重载
在Typescript
中,函数重载允许我们为同一个函数提供多个不同的函数签名,以便于在调用函数时能够根据参数的类型或者数量
来自动选择合适的函数实现。通过函数重载,我们可以实现函数的“多态性”,使得函数能够灵活地处理不同的参数类型和返回值类型。函数重载通常是通过多次定义同一个函数名,每次定义都包含不同的参数类型或者数量,但是具有相同的函数名,Typescript
能够根据提供的参数类型或者数量来匹配合适的函数实现!
1 | interface IdLabel{ |
🥹 在上述的例子中,我们定义了一个名为createLabel
的函数,它有2个重载签名和一个实现函数体,实现函数体将根据参数的类型来执行不同的逻辑!当调用createLabel
函数的时候,Typescript
将根据提供的参数类型自动选择合适的重载签名,并调用对应的函数实现!
⚠ 在定义函数重载的时候,必须要覆盖到函数调用所可能涉及到的全部场景,以确保在调用函数时能够根据参数类型或者数量自动选择合适的函数签名!
当条件类型遇上函数重载
原本js中只需要定义一个方法,现在因为参数或者类型的不同,导致我们需要一遍又一遍地做出相同类型的选择,做对应的函数重载,而当出现新的类型的时候,有可能需要重载定义的函数也会相应地增加, 😵💫 那么,是否可以对这个进行优化呢?
1 | // 定义一泛型条件类型NameOrId |
:+1: 通过约束类型的泛型参数以及条件类型,我们可以将原本需要重载多次才能够实现的函数定义,浓缩到一个无须重载的单个函数中来!
映射类型
索引签名
索引签名
用于声明尚未提前声明的属性的类型,在Typescript
中用于描述对象的索引类型的方式,允许定义对象的索引键(通常是字符串或者数字),以及对应索引键所对应的值的类型
1 | interface StringArray { |
🤦 索引签名可以弄个来声明尚未提前声明的属性类型,这特别是在一些前后端对接的前端代码中比较常见,比如我们需要定义一个用户实体对象类型,以便于在编码过程中的一个自动提示功能,因此我们需要定义一个用户类型:
1 | interface UserInfo{ |
🌠 这里我们定义了一用户信息类型,通过[key: string]: any
来定义剩下的未知属性的对象类型
基于索引签名之上的映射类型
映射类型
建立在索引签名的语法之上,它是一种泛型类型,使用PropertyKeys
的并集(通常使用keyof
来获取)来迭代键创建新的类型
这听着有点绕,具体来看 👇 的一个例子:
1 | type OptionsFlag<Type> = { |
🤦 在上述的例子中,我们定义了一个泛型类型,通过作用于Type
类型上,从Type
中提取到所有的key,然后使用in
来迭代循环每一个key,然后将key所对应的值的类型声明为boolean类型的!
类型修改器
在映射key的期间,还可以应用两个附加修饰符:
readonly(设置属性只读)
和?(设置属性可选)
,可以通过+
或者-
来添加或者删除类型中属性的修饰符,默认是+
,如下所示:
1 | type CreateMutable<Type> = { |
🤦 这里将一类型中的readonly
修饰符给移除掉了!!
1 | type Concrete<Type> = { |
🤦 而这里则通过-?
将属性的可选修饰符给去掉了!
⚠ 注意这里的对修饰符的操作,其所在的顺序,也是使用修饰符的顺序,需保持一致!!!
key映射新名字
当我们在使用这个类型映射的时候,可以在迭代key的时候,对key进行重命名,映射为新的key,如下代码所示:
1 | // 下面使用as关键词来对属性Property进行重命名,NewKeyType则是一新的名称,这里可以使用模版字符串的方式来组合 |
:+1: 通过上述的方式,将一个类型,映射为对属性进行getter的类型,这里可以根据实际情况进行灵活的组装的方式!
😕 那么如果我想在映射的时候,过滤掉没有用的属性,从而成为另外一个新的属性,是否可行?
1 | type RemoveKindFiled<Type> = { |
⭐ 上面的例子通过结合as
以及Exclude(泛型工具,从属性中剔除kind)
来实现将一个类型中的kind属性给剔除掉,并返回新的的类型!
模版字符类型
模版字符串
一般建立在字符串文字类型的基础上,通过联合的方式来扩展为其他的字符串
1 | type World = 'world' |
⭐ 这里的类型Greeting
通过模版字符串的方式来引用另外一模版字符类型,合成一新的模版字符类型!
联合遇上模版字符类型
当模版字符遇上联合,对联合类型成员的每一个进行“笛卡尔组合”,如下所示
1 | type EmailLocaleIDs = "welcome_email" | "email_heading"; |
内部字符转换类型
Typescript
提供了关于针对字符类型进行相关转换的操作,主要有:
- 字符大写转换:
Uppercase<StringType>
; - 字符小写转换:
Lowercase<StringType>
; - 首字母大写转换:
Capitalize<StringType>
; - 首字母小写转换:
Uncapitalize<StringType>