前言

本文主要整理介绍关于Typescripe中对于类型的操作,从而创建出新的类型,也就是说无需从头到位来编写一类型,而是基于原始类型,借助于相关的类型操作符,以及组合其他的类型工具,来创建新的一类型!
TypeScript中的类型操作符

泛型Generics

泛型是一种类型变量的概念,可以帮助我们来编写更加灵活更加通用的代码, 😕 既然是“变量”,那么在定义的时候可以通过“传参”的方式来使用,只是这个参数它是一个类型参数,通过在需要的时候,往方法、接口、类中传递类型,即可知道当前的方法、接口、类即将作用在哪种类型上!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 泛型函数
function identity<Type>(arg: Type): Type{
return arg;
}
// 泛型接口
interface GenericIdentityFn<Type> {
Type: type;
<Type>(arg: Type): Type
}
// 泛型类
class GenericNumber<Type>{
zeroValue: Type;
add: (x: Type, y: Type) => Type;
}

范型约束

有两个以下的泛型函数:

1
2
3
4
5
6
function firstElement1<Type>(arr: Type[]) {
return arr[0];
}
function firstElement2<Type extends any[]>(arr: Type) {
return arr[0];
}

上述两个函数在定义上有一个细微的差别:firstElement1函数使用了一个泛型类型Type,表示函数可以接收任何类型的数组作为参数,并返回数组的第一个元素,函数参数arr是一个由泛型Type所组成的数组,而firstElement2函数则使用了一个**泛型约束extends any[]**,表示Type必须是一个数组,函数参数arr的类型是泛型Type,它可以是任何满足约束的数组类型,两者在使用上,几乎是一致的,都是获取数组的第一个元素,然而它们在类型推断和使用方式上有小小的区别:

  1. 对于firstElement1函数,由于参数arr是一个泛型数组类型Type[],因此在调用时无需显示地指定泛型类型,Typescript可以根据传递的数组类型自动推断出泛型类型,如下示例所示:
    1
    2
    const result1 = firstElement1([1, 2, 3]);// result1类型是number
    const result2 = firstElement1(['a', 'b', 'c']); //result2类型是string
  2. 对于firstElement2函数,参数arr是一个范型Type,它是一个可以任何满足约束的数组类型,由于这里泛型类型Type并没有指定任何具体的类型(Type extends any[]),因此,在调用的时候需要显示地制定泛型类型,如下代码所示:
    1
    2
    const 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
2
type Point = {x: number, y: number}
type P = keyof Point // type P = 'x' | 'y'

如果所作用的对象类型是一个索引访问类型的对象的话,那么keyof将会返回如下的类型:

1
2
3
4
5
type Arrayish = { [n: number]: unknown }
type A = keyof Arrayish; // type A = number

type Mapish = { [k: string]: boolean }
type M = keyof Mapish; // type M = string | number

👉Typescript中,keyof是一种类型操作符,用于获取类型中所有键(属性名)的联合类型,返回一个字符串或者数字类型的联合类型,表示指定类型的所有可访问键,使用keyof操作符可以让我们在编译时获取某个类型的所有属性名,然后在代码中使用这些属性名来返回对象的属性或者定义更加灵活的函数和类型,比如有 👇 的一个例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
interface Person {
name: string,
age: number
}
type PersonKey = keyof Person // 'name' | 'age'
// 定义一函数,用于从对象中获取属性
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K]{
return obj[key]
}
const person: Person = {
name: 'Koby',
age: 30
}
const nameValue: string = getProperty(person, 'name') // 正常访问name属性
const ageValue: number = getProperty(person, 'age') // 正常访问age属性
const genderValue = getProperty(person, 'gender') // 报错,gender并非person中的属性!

上述我们定义了一个函数getProperty,用于从某个对象中获取属性对应的值,如果属性名不存在于对象中的话,将在编译时报错

typeof操作符

javascript中本来就有这个typeof关键词,用于获取一个变量的类型,而在Typescript中,则使用typeof来引用变量或者属性的类型,可以获取一个值的类型而不需要实际运行代码!
单纯的将typeof作用于基本数据类型好像没多大用处,但是如果与其他类型运算符结合起来,可以方便地表达许多的模式,比如有下面的一个例子:

1
2
3
4
5
6
7
type Predicate = (x: unknown) => boolean
type K = ReturnType<Predicate> // type K = boolean
// 如果我们尝试使用`ReturnType`函数名称,将会看到一个指示性的错误
function f() {
return { x: 10, y: 3 }
}
type P = ReturnType<f>

typeof运用

Typescript中如果想要定义一个新的类型,不能直接将一个变量的值直接赋予类型,而必须要使用typeof关键词

索引访问类型

👇 有下面的这样的一个例子

1
2
3
4
5
6
type Person = { age: number, name: string, alive: boolean }
type Age = Person['age'] // type Age = number
type I1 = Person['age' | 'name'] // type I1 = string | number
type I2 = Person[keyof Person] // type I2 = string | number | boolean
// 如果通过索引来访问对象中不存在的属性时则直接报错
type I3 = Person['alve']

🌝 从上述的代码中可以看出,在Typescript中可以通过使用[key]对一个类型对象,从一个类型对象取一个/多个key来作为新的类型,一般是通过索引访问的方式来组成一个新的联合类型!

🤔 如果被作用的对象是一个数组对象的话,那么可以采用 👇 的方式来获取数组对象成员的类型

1
2
3
4
5
6
7
const MyArray = [
{ name: "Alice", age: 15 },
{ name: "Bob", age: 23 },
{ name: "Eve", age: 38 }
]
// 这里必须使用typeof,因为MyArray[number]是一个value,而我们定义的是类型
type Person = typeof MyArray[number]

条件类型

1
2
3
4
interface Animal {}
interface Dog extends Animal{}
type Example1 = Dog extends Animal ? number : string // type Example1 = number
type Example2 = RegExp extends Animal ? number : string // type Example2 = string

当左侧的类型extends可分配给右侧的类型时,将获得true分支逻辑,也就是number类型,否则将获得false分支逻辑,也就是string类型,这看起来有点类似于javascript中的“三目运算符”。
😕 那么这个这种分支类型有什么用途呢?在了解关于这种分支类型的用途之前,先来了解一下Typescript中的重载!

函数重载

Typescript中,函数重载允许我们为同一个函数提供多个不同的函数签名,以便于在调用函数时能够根据参数的类型或者数量来自动选择合适的函数实现。通过函数重载,我们可以实现函数的“多态性”,使得函数能够灵活地处理不同的参数类型和返回值类型。函数重载通常是通过多次定义同一个函数名,每次定义都包含不同的参数类型或者数量,但是具有相同的函数名Typescript能够根据提供的参数类型或者数量来匹配合适的函数实现!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
interface IdLabel{
id: number
}
interface NameLabel{
name: string
}
function createLabel(id: number): IdLabel;
function createLabel(name: string): NameLabel;
function createLabel(nameOrId: string | number): IdLabel | NameLabel {
if(typeof nameOrId === 'number'){
return nameOrId * 2
}else if(typeof nameOrId === 'string'){
return nameOrId
}
}
const result1 = createLabel(10); // result1的类型是number
const result2 = createLabel('hello') // result2的类型是string

:face_holding_back_tears: 在上述的例子中,我们定义了一个名为createLabel的函数,它有2个重载签名和一个实现函数体,实现函数体将根据参数的类型来执行不同的逻辑!当调用createLabel函数的时候,Typescript将根据提供的参数类型自动选择合适的重载签名,并调用对应的函数实现!
在定义函数重载的时候,必须要覆盖到函数调用所可能涉及到的全部场景,以确保在调用函数时能够根据参数类型或者数量自动选择合适的函数签名!

当条件类型遇上函数重载

原本js中只需要定义一个方法,现在因为参数或者类型的不同,导致我们需要一遍又一遍地做出相同类型的选择,做对应的函数重载,而当出现新的类型的时候,有可能需要重载定义的函数也会相应地增加, :face_with_spiral_eyes: 那么,是否可以对这个进行优化呢?

1
2
3
4
5
6
// 定义一泛型条件类型NameOrId
type NameOrId<T extends string | number> = T extends string ? NameLabel : IdLabel
// 根据这个泛型条件类型来重新定义我们的函数
function createLabel<T extends string | number>(nameOrId: T){
// 这里省略对应的函数体内容
}

:+1: 通过约束类型的泛型参数以及条件类型,我们可以将原本需要重载多次才能够实现的函数定义,浓缩到一个无须重载的单个函数中来!

映射类型

索引签名

索引签名用于声明尚未提前声明的属性的类型,在Typescript中用于描述对象的索引类型的方式,允许定义对象的索引键(通常是字符串或者数字),以及对应索引键所对应的值的类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
interface StringArray {
[index: number]: string;
}
inerface Dictionary {
[key: string]: string;
}
// 定义一StringArray数组对象,代表可通过number类型索引键来访问其成员,返回值类型为string
const myArray: StringArray = ['hello', 'world']
const firstElement: string = myArray[0]
// 定义一Dictionary对象,代表可通过string类型索引键来返回其成员
const myDict: Dictionary{
'a': 'a',
'b': 'b',
'c': 'c'
}
const valueOfA: string = myDict['a']

🤦 索引签名可以弄个来声明尚未提前声明的属性类型,这特别是在一些前后端对接的前端代码中比较常见,比如我们需要定义一个用户实体对象类型,以便于在编码过程中的一个自动提示功能,因此我们需要定义一个用户类型:

1
2
3
4
5
interface UserInfo{
name: string;
age: number;
[key: string]: any;
}

🌠 这里我们定义了一用户信息类型,通过[key: string]: any来定义剩下的未知属性的对象类型

基于索引签名之上的映射类型

映射类型建立在索引签名的语法之上,它是一种泛型类型,使用PropertyKeys的并集(通常使用keyof来获取)来迭代键创建新的类型
这听着有点绕,具体来看 👇 的一个例子:

1
2
3
4
5
6
7
8
type OptionsFlag<Type> = {
[Property in keyof Type]: boolean;
}
type Features = {
darkMode: () => void;
newUserProfile: () => void;
}
type FeatureOptions = OptionsFlag<Features>; // type FeatureOptions = { darkMode: boolean, newUserProfile: boolean }

🤦 在上述的例子中,我们定义了一个泛型类型,通过作用于Type类型上,从Type中提取到所有的key,然后使用in来迭代循环每一个key,然后将key所对应的值的类型声明为boolean类型的!

类型修改器

在映射key的期间,还可以应用两个附加修饰符:readonly(设置属性只读)?(设置属性可选),可以通过+或者-来添加或者删除类型中属性的修饰符,默认是+,如下所示:

1
2
3
4
5
6
7
8
9
type CreateMutable<Type> = {
-readonly [Property in keyof Type]: Type[Property]
}
type LockedAccount = {
readonly id: string;
readonly name: string;
}
type UnlockedAccount = CreateMutable<LockedAccount>;
// type UnlockedAccount = { id: string; name: string }

🤦 这里将一类型中的readonly修饰符给移除掉了!!

1
2
3
4
5
6
7
8
9
10
type Concrete<Type> = {
[Property in keyof Type]-?: Type[Property];
}
type MaybeUser = {
id: string;
name?: string;
age?: string;
}
type User = Concrete<MaybeUser>;
// type User = { id: string, name: string, age: string }

🤦 而这里则通过-?将属性的可选修饰符给去掉了!

注意这里的对修饰符的操作,其所在的顺序,也是使用修饰符的顺序,需保持一致!!!

key映射新名字

当我们在使用这个类型映射的时候,可以在迭代key的时候,对key进行重命名,映射为新的key,如下代码所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 下面使用as关键词来对属性Property进行重命名,NewKeyType则是一新的名称,这里可以使用模版字符串的方式来组合
type MappedTypeWithNewProperties<Type> = {
[Property in keyof Type as NewKeyType]: Type[Property]
}
// 比如有下面的一例子
type Getters<Type> = {
[Property in keyof Type as `get${Capitalize<string & Property>}`]: () => Type[Property]
}
interface Person{
name: string;
age: number;
location: string;
}
type LazyPerson = Getters<Person>
// type LazyPerson = { getName: () => string, getAge: () => number, getLocation: () => string }

:+1: 通过上述的方式,将一个类型,映射为对属性进行getter的类型,这里可以根据实际情况进行灵活的组装的方式!

😕 那么如果我想在映射的时候,过滤掉没有用的属性,从而成为另外一个新的属性,是否可行?

1
2
3
4
5
6
7
8
9
type RemoveKindFiled<Type> = {
[Property in keyof Type as Exclude<Property, 'kind'>]: Type[Property]
}
interface Circle {
kind: 'circle';
radius: number;
}
type KindlessCircle = RemoveKindField<Circle>
// type KindlessCircle = { radius: number }

上面的例子通过结合as以及Exclude(泛型工具,从属性中剔除kind)来实现将一个类型中的kind属性给剔除掉,并返回新的的类型!

模版字符类型

模版字符串一般建立在字符串文字类型的基础上,通过联合的方式来扩展为其他的字符串

1
2
type World = 'world'
type Greeting = `hello ${World}`

这里的类型Greeting通过模版字符串的方式来引用另外一模版字符类型,合成一新的模版字符类型!

联合遇上模版字符类型

当模版字符遇上联合,对联合类型成员的每一个进行“笛卡尔组合”,如下所示

1
2
3
4
type EmailLocaleIDs = "welcome_email" | "email_heading";
type FooterLocaleIDs = "footer_title" | "footer_sendoff";
type AllLocaleIDs = `${EmailLocaleIDs | FooterLocaleIDs}_id`;
// type AllLocaleIDs = "welcome_email_id" | "email_heading_id" | "footer_title_id" | "footer_sendoff_id"

内部字符转换类型

Typescript提供了关于针对字符类型进行相关转换的操作,主要有:

  1. 字符大写转换:Uppercase<StringType>;
  2. 字符小写转换:Lowercase<StringType>;
  3. 首字母大写转换:Capitalize<StringType>;
  4. 首字母小写转换:Uncapitalize<StringType>