类型运算符和类型映射

2024/4/8 TypeScript

# 类型运算符

# keyof 运算符

​ keyof 是一个单目运算符,接受一个对象类型作为参数,返回该对象的所有键名组成的联合类型。

​ 由于JS对象的键名只有三种类型,所以对于任意对象的键名的联合类型就是string|number|symbol。

​ 对于没有自定义键名的类型使用 keyof 运算符,返回never类型,表示不可能有这样类型的键名。

type KeyT = keyof any; // string | number | symbol
type KeyT = keyof object; // never
1
2

​ 由于keyof返回的类型是string|number|symbol,如果有些场合只需要其中的一种类型,那么可以采用交叉类型的写法。type MyKeys<Obj extends object> = Capital<string & keyof Obj>;

如果对象属性名采用索引形式,keyof 会返回属性名的索引类型。

如果 keyof 运算符用于数组或元组类型,会返回数组的所有键名,包括数字键名和继承的键名。

对于联合类型,keyof 返回成员共有的键名。

对于交叉类型,keyof 返回所有键名。

keyof 取出的是键名组成的联合类型,如果想取出键值组成的联合类型,可以像下面这样写。

type MyObj = {
  foo: number,
  bar: string,
};
type Keys = keyof MyObj; // Keys是键名组成的联合类型
// MyObj[Keys]会取出每个键名对应的键值类型,组成一个新的联合类型
type Values = MyObj[Keys]; // number|string
1
2
3
4
5
6
7

# keyof 运算符的用途

​ keyof 运算符往往用于精确表达对象的属性类型。另一个用途是用于属性映射,即将一个类型的所有属性逐一映射成其他值。

# in 运算符

​ in运算符用来确定对象是否包含某个属性名。

​ TS语言的类型运算中,in运算符有不同的用法,用来取出(遍历)联合类型的每一个成员类型。

# 方括号运算符

​ 方括号运算符([])用于取出对象的键值类型,比如T[K]会返回对象T的属性K的类型。

​ 方括号的参数如果是联合类型,那么返回的也是联合类型。

​ 如果访问不存在的属性,会报错。

​ 方括号运算符的参数也可以是属性名的索引类型。这个语法对于数组也适用,可以使用number作为方括号的参数。

​ 注意,方括号里面不能有值的运算。

# extends...?: 条件运算符

​ 条件运算符 extends...?: 可以根据当前类型是否符合某种条件,返回不同的类型。

/* extends用来判断,类型T是否可以赋值给类型U,即T是否为U的子类型,这里的T和U可以是任意类型。
   如果T能够赋值给类型U,表达式的结果为类型X,否则结果为类型Y。 */
T extends U ? X : Y
1
2
3

​ 如果需要判断的类型是一个联合类型,那么条件运算符会展开这个联合类型。

(A|B) extends U ? X : Y
// 等同于     相当于A和B分别进行运算符,返回结果组成一个联合类型
(A extends U ? X : Y) |
(B extends U ? X : Y)
1
2
3
4

​ 如果不希望联合类型被条件运算符展开,可以把extends两侧的操作数都放在方括号里面。

// 示例一。类型参数是一个联合类型,所以会被展开,返回的也是联合类型
type ToArray<Type> =
  Type extends any ? Type[] : never;
type T = ToArray<string|number>; // string[]|number[]
// 示例二。两侧的运算数都放在方括号里面,所以传入的联合类型不会展开,返回的是一个数组
type ToArray<Type> =
  [Type] extends [any] ? Type[] : never;
type T = ToArray<string|number>; // (string | number)[]
1
2
3
4
5
6
7
8

​ 条件运算符还可以嵌套使用。

# infer 关键字

​ infer关键字用来定义泛型里面推断出来的类型参数,而不是外部传入的类型参数。它通常跟条件运算符一起使用,用在extends关键字后面的父类型之中。

# is 运算符

​ 函数返回布尔值的时候,可以使用is运算符,限定返回值与参数之间的关系。is运算符用来描述返回值属于true还是false。

​ is运算符总是用于描述函数的返回值类型,写法采用parameterName is Type的形式,即左侧为当前函数的参数名,右侧为某一种类型。它返回一个布尔值,表示左侧参数是否属于右侧的类型。

​ is运算符可以用于类型保护。

​ is运算符还有一种特殊用法,就是用在类(class)的内部,描述类的方法的返回值。

# 模板字符串

​ TypeScript 允许使用模板字符串,构建类型。模板字符串的最大特点,就是内部可以引用其他类型。注意,模板字符串可以引用的类型一共7种,分别是 string、number、bigint、boolean、null、undefined、Enum。引用这7种以外的类型会报错。

​ 模板字符串里面引用的类型,如果是一个联合类型,那么它返回的也是一个联合类型,即模板字符串可以展开联合类型。

​ 如果模板字符串引用两个联合类型,它会交叉展开这两个类型。

# satisfies 运算符

​ satisfies运算符用来检测某个值是否符合指定类型。有时候,不方便将某个值指定为某种类型,但是希望这个值符合类型条件,这时候就可以用satisfies运算符对其进行检测。

​ satisfies可以检测属性名,也可以检测属性值。

# 类型映射

​ 映射指的是,将一种类型按照映射规则,转换成另一种类型,通常用于对象类型。

type A = {
  foo: number;
  bar: number;
};
type B = { // 类型B采用了属性名索引的写法
  [prop in keyof A]: string; // 得到类型A的所有属性名,然后将每个属性的类型改成string
};
/* 得到
type B = {
  foo: string;
  bar: string;
};
*/
type C = { // 类型C原样复制了类型A
  [prop in keyof A]: A[prop];
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

​ 在语法上,[prop in keyof A]是一个属性名表达式,表示这里的属性名需要计算得到。具体的计算规则如下:

  • prop:属性名变量,名字可以随便起。
  • in:运算符,用来取出右侧的联合类型的每一个成员。
  • keyof A:返回类型A的每一个属性名,组成一个联合类型。

​ 为了增加代码复用性,可以把常用的映射写成泛型。

type ToBoolean<Type> = {
  [Property in keyof Type]: boolean;
}; // 定义了一个泛型,可以将其他对象的所有属性值都改成 boolean 类型
1
2
3

​ 不使用联合类型,直接使用某种具体类型进行属性名映射也是可以的。甚至还可写成p in string。

type MyObj = {
  [p in 'foo']: number; // 可看成只有一个成员的联合类型,因此得到了只有这一个属性的对象类型
};
// 等同于
type MyObj = {
  foo: number;
};
type MyObj = {
  [p in string]: boolean; //[p in string]就是属性名索引形式[p: string]的映射写法
};
// 等同于
type MyObj = {
  [p: string]: boolean;
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14

​ 通过映射,可以把某个对象的所有属性改成可选属性。

type A = {
  a: string;
  b: number;
};
type B = { // 类型B在类型A的所有属性名后面添加问号,使得这些属性都变成了可选属性
  [Prop in keyof A]?: A[Prop];
};
1
2
3
4
5
6
7

​ 事实上,TS的内置工具类型Partial<T>,就是这样实现的。内置的工具类型Readonly<T>可以将所有属性改为只读属性,实现也是通过映射。

type Readonly<T> = {
  readonly [P in keyof T]: T[P]; // 将 T 的所有属性改为只读属性
};
// 它的用法如下
type T = { a: string; b: number };
type ReadonlyT = Readonly<T>;
1
2
3
4
5
6

# 映射修饰符

​ 映射会原样复制原始对象的可选属性和只读属性。TypeScript 引入了两个映射修饰符,用来在映射时添加或移除某个属性的?修饰符和readonly修饰符。

    • 修饰符:写成+?或+readonly,为映射属性添加?修饰符或readonly修饰符。
  • – 修饰符:写成-?或-readonly,为映射属性移除?修饰符或readonly修饰符。
  • 注意,+?或-?要写在属性名的后面。+readonly和-readonly要写在属性名的前面。
  • +?修饰符可以简写成?,+readonly修饰符可以简写成readonly。
  • –?修饰符移除了可选属性以后,该属性就不能等于undefined了,实际变成必选属性了。但是,这个修饰符不会移除null类型。
// 增加
type MyObj<T> = {
  +readonly [P in keyof T]+?: T[P];
};
// 移除
type MyObj<T> = {
  -readonly [P in keyof T]-?: T[P];
}
1
2
3
4
5
6
7
8

# 键名重映射

# 语法

​ 键名重映射,允许改变键名。键名重映射的语法是在键名映射的后面加上as + 新类型子句。这里的“新类型”通常是一个模板字符串,里面可以对原始键名进行各种操作。

type A = {
  foo: number;
  bar: number;
};
type B = {
  [p in keyof A as `${p}ID`]: number;
};
// 等同于
type B = {
  fooID: number;
  barID: number;
};
1
2
3
4
5
6
7
8
9
10
11
12

# 属性过滤

​ 键名重映射还可以过滤掉某些属性。

type User = {
  name: string,
  age: number
}
type Filter<T> = { // 过滤不符合条件的属性,只保留属性值为字符串的属性
  [K in keyof T // 映射K in keyof T获取类型T的每一个属性以后,然后使用as Type修改键名
    as T[K] extends string ? K : never]: string 
}  // 如果属性值T[K]的类型是字符串,那么属性名不变,否则属性名类型改为never(该属性名不存在)
type FilteredUser = Filter<User> // { name: string }
1
2
3
4
5
6
7
8
9

# 联合类型的映射typescript

​ 由于键名重映射可以修改键名类型,所以原始键名的类型不必是string|number|symbol,任意的联合类型都可以用来进行键名重映射。

上次更新: 2024/7/27 12:43:06