likes
comments
collection
share

TypeScript:入门与进阶(下)

作者站长头像
站长
· 阅读数 14

文章摘录自:


六、类型系统的层级

类型层级指的是:TypeScript 中所有类型的兼容关系,从最上面一层的 any 类型,到最底层的 never 类型。

判断类型兼容性的方式:

type Result = 'linbudu' extends string ? 1 : 2;

如果返回 1 ,则说明 'linbudu' 为 string 的子类型。否则,说明不成立。但注意,不成立并不意味着 string 就是 'linbudu' 的子类型了。还有一种备选的,通过赋值来进行兼容性检查的方式,其大致使用方式是这样的:

declare let source: string;

declare let anyType: any;
declare let neverType: never;

anyType = source;

// 不能将类型“string”分配给类型“never”。
neverType = source;// error

对于变量 a = 变量 b,如果成立,意味着 <变量 b 的类型> extends <变量 a 的类型> 成立,即 b 类型是 a 类型的子类型,在这里即是 string extends never ,这明显是不成立的。

类型系统的层级规则有:

1、字面量类型 < 对应的原始类型

type Result1 = "linbudu" extends string ? 1 : 2; // 1
type Result2 = 1 extends number ? 1 : 2; // 1
type Result3 = true extends boolean ? 1 : 2; // 1

2、字面量类型 < 包含此字面量类型的联合类型,原始类型 < 包含此原始类型的联合类型

type Result8 = 'lin' extends 'lin' | 'bu' | 'du' ? 1 : 2; // 1

type Result10 = string extends string | false | number ? 1 : 2; // 1

3、同一基础类型的字面量联合类型 < 此基础类型

type Result11 = 'lin' | 'bu' | 'budu' extends string ? 1 : 2; // 1 
type Result12 = {} | (() => void) | [] extends object ? 1 : 2; // 1

合并第二点和第三点可得出: 4、字面量类型 < 包含此字面量类型的联合类型(同一基础类型) < 对应的原始类型

// 2
type Result13 = 'linbudu' extends 'linbudu' | '599'
  ? 'linbudu' | '599' extends string
    ? 2
    : 1
  : 0;

5、原始类型 < 原始类型对应的装箱类型 < Object 类型

type Result14 = string extends String ? 1 : 2; // 1
type Result15 = String extends {} ? 1 : 2; // 1
type Result16 = {} extends object ? 1 : 2; // 1
type Result18 = object extends Object ? 1 : 2; // 1

6、Object < any / unknown

type Result22 = Object extends any ? 1 : 2; // 1
type Result23 = Object extends unknown ? 1 : 2; // 1

type Result31 = any extends unknown ? 1 : 2;  // 1
type Result32 = unknown extends any ? 1 : 2;  // 1

七、条件类型与infer

7.1、条件类型

条件类型直通车

条件类型的语法类似平时常用的三元表达式,它的基本语法如下(伪代码):

TypeA extends TypeB ? Result1 : Result2;

但需要注意的是,条件类型中使用 extends 判断类型的兼容性,而判断类型的全等性。这是因为在类型层面中,对于能够进行赋值操作的两个变量,我们并不需要它们的类型完全相等,只需要具有兼容性,而两个完全相同的类型,其 extends 自然也是成立的。

type LiteralType<T> = T extends string ? "string" : "other";

type Res1 = LiteralType<"linbudu">; // "string"
type Res2 = LiteralType<599>; // "other"

type LiteralType_new<T> = T extends string ? T : "other";

type Res1_new = LiteralType_new<"linbudu">; // "linbudu"

条件类型中也常见多层嵌套

export type LiteralType<T> = T extends string
	? "string"
	: T extends number
	? "number"
	: T extends boolean
	? "boolean"
	: T extends null
	? "null"
	: T extends undefined
	? "undefined"
	: never;

type Res1 = LiteralType<"linbudu">; // "string"
type Res2 = LiteralType<599>; // "number"
type Res3 = LiteralType<true>; // "boolean"

条件类型还可以用来对更复杂的类型进行比较,比如函数类型:

type Func = (...args: any[]) => any;

type FunctionConditionType<T extends Func> = T extends (
  ...args: any[]
) => string
  ? 'A string return func!'
  : 'A non-string return func!';

//  "A string return func!"
type StringResult = FunctionConditionType<() => string>;
// 'A non-string return func!';
type NonStringResult1 = FunctionConditionType<() => boolean>;

在这里,我们的条件类型用于判断两个函数类型是否具有兼容性,而条件中并不限制参数类型,仅比较二者的返回值类型

与此同时,存在泛型约束和条件类型两个 extends ,但它们产生作用的时机完全不同,泛型约束要求你传入符合结构的类型参数,相当于参数校验。而条件类型使用类型参数进行条件判断(就像 if else),相当于实际内部逻辑

7.2、infer

typescript 中 ,用infer 关键字来在条件类型中提取类型的某一部分信息。

infer与 extends 和 三元运算符 组合使用,用于推断某个复杂类型的部分,简单的说,就是用来推导泛型参数

使用规则:

type ParamsArray<T> = T extends Array<infer P> ? P : T;
  1. inter 只能出现在 extends 关键字的右侧;
  2. inter P 可以理解成数学上的未知数 x
  3. 其中 extends 关键字的作用,是用来判断 右边的类型 是否兼容 左边的泛型 T,如果兼容则返回 ? 后面的内容,否则返回 : 后面的内容。

分析type ParamsArray<T> = T extends Array<infer P> ? P : T;代码可知, Array<infer P> 的类型是否能够兼容传入的泛型,如果兼容,则返回 P,也就是 Array 类型的泛型;如果不兼容,直接返回传入的泛型。

在函数类型中的使用:

type Func = (...args: any[]) => any;
type FunctionReturnType<T extends Func> = T extends (
  ...args: any[]
) => infer R
  ? R
  : never;
  
type StringResult = FunctionReturnType<() => string>;//string
type NumberResult = FunctionReturnType<() => number>;//number

// error 类型“boolean”不满足约束 “Func”
type booleanResult = FunctionReturnType<boolean>;

上面的代码其实表达了,当传入的类型参数满足 T extends (...args: any[] ) => infer R 这样一个结构(不用管 infer R,当它是 any 就行),返回 infer R 位置的值,即 R。否则,返回 never

在数组类型中的使用:

type Swap<T extends any[]> = T extends [infer A, infer B] ? [B, A] : T;

type SwapResult1 = Swap<[1, 2]>; // 符合元组结构,首尾元素替换[2, 1]
type SwapResult2 = Swap<[1, 2, 3]>; // 不符合结构,没有发生替换,仍是 [1, 2, 3]

但我们可以使用 rest 操作符来处理任意长度的情况:

// 提取首尾两个
type ExtractStartAndEnd<T extends any[]> = T extends [
  infer Start,
  ...any[],
  infer End
]
  ? [Start, End]
  : T;

// 调换首尾两个
type SwapStartAndEnd<T extends any[]> = T extends [
  infer Start,
  ...infer Left,
  infer End
]
  ? [End, ...Left, Start]
  : T;

// 调换开头两个
type SwapFirstTwo<T extends any[]> = T extends [
  infer Start1,
  infer Start2,
  ...infer Left
]
  ? [Start2, Start1, ...Left]
  : T;

在接口中的使用:

// 提取对象的属性类型
type PropType<T, K extends keyof T> = T extends { [key in K]: infer R }
  ? R
  : never;

type PropTypeResult1 = PropType<{ name: string }, 'name'>; // string
type PropTypeResult2 = PropType<{ name: string; age: number }, 'name' | 'age'>; // string | number

// 反转键名与键值
type ReverseKeyValue<T extends Record<string, unknown>> = T extends Record<infer K, infer V> ? Record<V & string, K> : never

type ReverseKeyValueResult1 = ReverseKeyValue<{ "key": "value" }>; // { "value": "key" }

在这里,为了体现 infer 作为类型工具的属性,我们结合了索引类型与映射类型,以及使用 & string 来确保属性名为 string 类型的小技巧。

如果不使用,会出现如下错误: TypeScript:入门与进阶(下)

这是因为,泛型参数 V 的来源是从键值类型推导出来的,TypeScript 中这样对键值类型进行 infer 推导,将导致类型信息丢失,而不满足索引签名类型只允许 string | number | symbol 的要求。

7.3、分布式条件类型

件类型分布式起作用的条件:

  • 首先,类型参数需要是一个联合类型
  • 其次,类型参数需要通过泛型参数的方式传入,而不能直接进行条件类型判断
  • 最后,条件类型中的泛型参数不能被包裹

八、内置工具类型

官方文档

内置的工具类型按照类型操作的不同,其实也可以大致划分为这么几类:

  • 属性修饰工具类型:对属性的修饰,包括对象属性和数组元素的可选/必选、只读/可写
  • 结构工具类型:对既有类型的裁剪、拼接、转换等
  • 集合工具类型:对集合(即联合类型)的处理,即交集、并集、差集、补集
  • 模式匹配工具类型:基于 infer 的模式匹配,即对一个既有类型特定位置类型的提取
  • 模板字符串工具类型:板字符串专属的工具类型

8.1、属性修饰工具类型

这一部分的工具类型主要使用属性修饰映射类型索引类型相关(索引类型签名、索引类型访问、索引类型查询均有使用)。

在内置工具类型中,访问性修饰工具类型包括以下三位:PartialRequiredReadonly

8.1.1 Partial

Partial? ,即标记属性为可选

type Partial<T> = {
  [P in keyof T]?: T[P];
};

其实 Partial 也可以使用 +? 来显式的表示添加可选标记:

type Partial<T> = {
  [P in keyof T]+?: T[P];
};

需要注意的是,可选标记不等于修改此属性类型为 原类型 | undefined

8.1.2 Required

Partial 与 Required 可以认为是一对工具类型,它们的功能是相反的。

Partial? ,即标记属性为可选,而 Required 则是 -?,相当于在原本属性上如果有 ? 这个标记,则移除它。

type Required<T> = {
  [P in keyof T]-?: T[P];
};

8.1.3 Readonly

type Readonly<T> = {
  readonly [P in keyof T]: T[P];
};

而类似 +?Readonly 中也可以使用 +readonly

type Readonly<T> = {
  +readonly [P in keyof T]: T[P];
};

也可以使用 -readonly将属性中的 readonly 修饰移除

type Mutable<T> = {
  -readonly [P in keyof T]: T[P];
};

8.2 结构工具类型

这一部分的工具类型主要使用条件类型以及映射类型索引类型

结构工具类型其实又可以分为两类,结构声明结构处理

8.2.1 结构声明类型-Record

type Record<K extends keyof any, T> = {
  [P in K]: T;
};

其中,K extends keyof any 即为键的类型,这里使用 extends keyof any 表明,传入的 K 可以是单个类型,也可以是联合类型,而 T 即为属性的类型

// 键名均为字符串,键值类型未知
type Record1 = Record<string, unknown>;
// 键名均为字符串,键值类型任意
type Record2 = Record<string, any>;
// 键名为字符串或数字,键值类型任意
type Record3 = Record<string | number, any>;

其中,Record<string, unknown>Record<string, any> 是日常使用较多的形式,通常我们使用这两者来代替 object

8.2.2 结构处理类型-Pick(选取)

Pick基类型作为第一个参数,将我们想要从基类型中选取的键的并集作为第二个参数:

type Pick<T, K extends keyof T> = {
  [P in K]: T[P];
};

比如,从原来的类型中选取几个类型作为新的类型输出:

interface Foo {
  name: string;
  age: number;
  gender: string
}

type PickedFoo = Pick<Foo, "name" | "age">

上面等价于:

type Pick<T> = {
  [P in "name" | "age"]: T[P];
};

8.2.3 结构处理类型-Omit(排除)

OmitPick 相反,第一个参数是基类型,第二个参数是从基类型中移除的键的并集:

type Omit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>;

interface Foo {
  name: string;
  age: number;
  gender: string;
}

type OmitFoo = Omit<Foo, "name" | "age">

// 等价于
// type OmitFoo = {
//   gender: string
// }

Exclude<A, B> 的结果就是联合类型 A 中不存在于 B 中的部分。因此,在这里 Exclude<keyof T, K> 其实就是 T 的键名联合类型中剔除了 K 的部分,将其作为 Pick 的键名,就实现了剔除一部分类型的效果。

8.2.4 剔除null、undefined - NonNullable

从联合类型中剔除 null | undefined 的工具类型 NonNullable

type NonNullable<T> = T extends null | undefined ? never : T;

8.2.5 拓展:Pick和Omit基于键值

Pick 和 Omit都是基于键名实现的,那基于键值呢?

interface Foo {
  name: string;
  age: number;
  isAdmin: boolean;
  info: {
    desc: string;
  };
  func: () => void;
  c?: () => void;
}

// 根据键值提取对应的键名
type PickKeysWithType<O, T> = {
  [P in keyof O]: O[P] extends T ? P : never;
}[keyof O];

// 如果没有可选属性,则是"name"|"isAdmin"
type Foo1 = PickKeysWithType<Foo, string | boolean>;// "name"|"isAdmin"|undefined

// 根据键名获取键值后返回新的类型
type PickWithType<O, T> = {
  [P in PickKeysWithType<O, T>]: O[P];
};

type Foo2 = PickWithType<Foo, string | boolean>;

// 等价于
// type Foo2 = {
//   name: string;
//   age: number;
// }

type Foo3 = PickWithType<Foo, (...arg: any) => any>;

// 等价于
// type Foo3 = {
//   func: () => void;
// }

// 挑选指定类型的属性 并约束 可选K
type PickWithType2<O, T, K extends PickKeysWithType<O, T>> = {
  [P in K]: O[P];
};
// 会报错 因为c 是可选的
type Foo4 = PickWithType2<Foo, (...arg: any) => any, 'func' | 'c'>;

8.3 集合工具类型

通常存在交集、并集、差集、补集这么几种情况。

  • 并集,两个集合的合并,合并时重复的元素只会保留一份(这也是联合类型的表现行为)。
  • 交集,两个集合的相交部分,即同时存在于这两个集合内的元素组成的集合。
  • 差集,对于 A、B 两个集合来说,A 相对于 B 的差集即为 A 中独有而 B 中不存在的元素 的组成的集合,或者说 A 中剔除了 B 中也存在的元素以后,还剩下的部分
  • 补集,补集是差集的特殊情况,此时集合 B 为集合 A 的子集,在这种情况下 A 相对于 B 的差集 + B = 完整的集合 A
// 并集
export type Concurrence<A, B> = A | B;

// 交集
export type Intersection<A, B> = A extends B ? A : never;

// 差集
export type Difference<A, B> = A extends B ? never : A;

// 补集
export type Complement<A, B extends A> = Difference<A, B>;

8.3.1、交集-Extract

type Extract<T, U> = T extends U ? T : never;

这里的具体实现其实就是条件类型的分布式特性,即当 TU 都是联合类型(视为一个集合)时,T 的成员会依次被拿出来进行 extends U ? T1 : T2 的计算,然后将最终的结果再合并成联合类型

其运行逻辑是这样的:

type AExtractB = Extract<1 | 2 | 3, 1 | 2 | 4>; // 1 | 2

type _AExtractB =
  | (1 extends 1 | 2 | 4 ? 1 : never) // 1
  | (2 extends 1 | 2 | 4 ? 2 : never) // 2
  | (3 extends 1 | 2 | 4 ? 3 : never); // never

8.3.2、差集-Exclude

差集 Exclude 与交集 Extract,但需要注意的是,差集存在相对的概念,即 A 相对于 B 的差集与 B 相对于 A 的差集并不一定相同,而交集则一定相同。

type Exclude<T, U> = T extends U ? never : T;

差集的运行逻辑:

type SetA = 1 | 2 | 3 | 5;

type SetB = 0 | 1 | 2 | 4;

type AExcludeB = Exclude<SetA, SetB>; // 3 | 5
type BExcludeA = Exclude<SetB, SetA>; // 0 | 4

type _AExcludeB =
  | (1 extends 0 | 1 | 2 | 4 ? never : 1) // never
  | (2 extends 0 | 1 | 2 | 4 ? never : 2) // never
  | (3 extends 0 | 1 | 2 | 4 ? never : 3) // 3
  | (5 extends 0 | 1 | 2 | 4 ? never : 5); // 5

type _BExcludeA =
  | (0 extends 1 | 2 | 3 | 5 ? never : 0) // 0
  | (1 extends 1 | 2 | 3 | 5 ? never : 1) // never
  | (2 extends 1 | 2 | 3 | 5 ? never : 2) // never
  | (4 extends 1 | 2 | 3 | 5 ? never : 4); // 4

8.4 模式匹配工具类型

这一部分的工具类型主要使用条件类型infer 关键字

对函数类型签名的模式匹配:

type FunctionType = (...args: any) => any;

// 匹配函数的参数类型
type Parameters<T extends FunctionType> = T extends (...args: infer P) => any ? P : never;

// 匹配函数返回值类型
type ReturnType<T extends FunctionType> = T extends (...args: any) => infer R ? R : any;

// 只匹配第一个参数类型
type FirstParameter<T extends FunctionType> = T extends (
  arg: infer P,
  ...args: any
) => any
  ? P
  : never;

对 Class 进行模式匹配的工具类型:

type ClassType = abstract new (...args: any) => any;

type ConstructorParameters<T extends ClassType> = T extends abstract new (
  ...args: infer P
) => any
  ? P
  : never;

type InstanceType<T extends ClassType> = T extends abstract new (
  ...args: any
) => infer R
  ? R
  : any;

Class 也可以用接口来进行声明:

export interface ClassType<TInstanceType = any> {
    new (...args: any[]): TInstanceType;
}

TypeScript 4.7 就支持了 infer 约束功能来实现对特定类型地提取

比如:只想要数组第一个为字符串的成员,如果第一个成员不是字符串类型就不要:

先写一个提取数组第一个成员的工具类型:

type FirstArrayItemType<T extends any[]> = T extends [infer P, ... any[]] ? P : never;

type Tmp1 = FirstArrayItemType<[599, 'linbudu']>; // 599
type Tmp2 = FirstArrayItemType<['linbudu', 599]>; // 'linbudu'

加上对提取字符串的条件类型:

type FirstArrayItemType<T extends any[]> = T extends [infer P extends string, ...any[]]
  ? P
  : never;

  type Tmp1 = FirstArrayItemType<[599, 'linbudu']>; // never
  type Tmp2 = FirstArrayItemType<['linbudu', 599]>; // 'linbudu'
  type Tmp3 = FirstArrayItemType<['linbudu']>; // 'linbudu'

8.5 模板字符串类型

8.5.1 基本用法

官方文档

1. 基础使用:

type World = 'World';

// "Hello World"
type Greeting = `Hello ${World}`;

这里的 Greeting 就是一个模板字符串类型,它内部通过与 JavaScript 中模板字符串相同的语法(${}),使用了另一个类型别名 World,其最终的类型就是将两个字符串类型值组装在一起返回

2. 通过泛型参数传入

除了使用确定的类型别名以外,模板字符串类型当然也支持通过泛型参数传入。并不是所有值都能被作为模板插槽,目前有效的类型只有 string | number | boolean | null | undefined | bigint 这几个:

type Greet<T extends string | number | boolean | null | undefined | bigint> = `Hello ${T}`;

type Greet1 = Greet<"linbudu">; // "Hello linbudu"
type Greet2 = Greet<599>; // "Hello 599"
type Greet3 = Greet<true>; // "Hello true"
type Greet4 = Greet<null>; // "Hello null"
type Greet5 = Greet<undefined>; // "Hello undefined"
type Greet6 = Greet<0x1fffffffffffff>; // "Hello 9007199254740991"

3. 直接为插槽传入一个类型而非类型别名

type Greeting = `Hello ${string}`;

在这种情况下,Greeting 类型并不会变成 Hello string,此时就是一个无法改变的模板字符串类型所有 Hello 开头的字面量类型 都会被视为 Hello ${string} 的子类型,如 Hello LinbuduHello TypeScript

模板字符串类型的主要目的即是增强字符串字面量类型的灵活性,进一步增强类型和逻辑代码的关联:

type Version = `${number}.${number}.${number}`;

const v1: Version = '1.1.0';

// error: 不能将类型 "1.0" 赋值给类型 `${number}.${number}.${number}`
const v2: Version = '1.0';

要声明大量存在关联的字符串字面量类型时,模板字符串类型也能在减少代码的同时获得更好的类型保障,比如,需要声明以下字符串类型时:

type SKU =
  | 'iphone-16G-official'
  | 'xiaomi-16G-official'
  | 'honor-16G-official'
  | 'iphone-16G-second-hand'
  | 'xiaomi-16G-second-hand'
  | 'honor-16G-second-hand'
  | 'iphone-64G-official'
  | 'xiaomi-64G-official'
  | 'honor-64G-official'
  | 'iphone-64G-second-hand'
  | 'xiaomi-64G-second-hand'
  | 'honor-64G-second-hand';

此时,可以利用 模板字符串类型的自动分发的特性来实现,它会将 所有插槽中的联合类型与剩余的字符串部分进行依次的排列组合

type Brand = 'iphone' | 'xiaomi' | 'honor';
type Memory = '16G' | '64G';
type ItemType = 'official' | 'second-hand';

type SKU = `${Brand}-${Memory}-${ItemType}`;

SKU 的类型,如图所示: TypeScript:入门与进阶(下)

除了直接在插槽中传递联合类型,通过泛型传入联合类型时同样会有分发过程:

type SizeRecord<Size extends string> = `${Size}-Record`;

type Size = 'Small' | 'Middle' | 'Large';

// "Small-Record" | "Middle-Record" | "Huge-Record"
type UnionSizeRecord = SizeRecord<Size>;

8.5.2 类型层级

由于模板字符串类型最终的产物还是字符串字面量类型,因此只要插槽位置的类型匹配,字符串字面量类型就可以被认为是模板字符串类型的子类型

declare let v1: `${number}.${number}.${number}`;
declare let v2: '1.2.4';

// 成立
v1 = v2;

// error: 不能将类型“${number}.${number}.${number}`”分配给“1.2.4”
v2 = v1

8.5.3 结合索引类型与映射类型

结合索引类型是基于 keyof + 模板字符串类型

interface Foo {
  name: string;
  age: number;
  job: () => void;
}

type ChangeListener = {
  on: (change: `${keyof Foo}Changed`) => void;
};

declare let listener: ChangeListener;

// 提示并约束为 "nameChanged" | "ageChanged" | "jobChanged"
listener.on('')

TypeScript:入门与进阶(下)

为了与映射类型实现更好的协作,TS 在引入模板字符串类型时支持了一个叫做 重映射(Remapping) 的新语法,基于模板字符串类型与重映射,我们可以实现新功能:在映射键名时基于原键名做修改

type Copy<T extends object> = {
  [K in keyof T]: T[K];
};

type CopyWithRename<T extends object> = {
  [K in keyof T as `modified_${string & K}`]: T[K]
}

interface Foo {
  name: string;
  age: number;
}

type CopiedFoo = CopyWithRename<Foo>;

// 等价于
type CopiedFooEqual = {
  modified_name: string;
  modified_age: number;
}

分析:

  • 通过 as 语法,将映射的键名作为变量,映射到一个新的字符串类型
  • 对象的合法键名类型包括了 symbol,而模板字符串类型插槽中并不支持 symbol 类型。因此我们使用 string & K 来确保了最终交由模板插槽的值,一定会是合法的 string 类型。

8.5.4 专用工具类型

这些工具类型专用于字符串字面量类型:

  • Uppercase :字符串大写
  • Lowercase :字符串小写
  • Capitalize :首字母大写
  • Uncapitalize :首字母小写

使用方法:

// 大写
type Heavy<T extends string> = `${Uppercase<T>}`
// 首字母大写
type Respect<T extends string> = `${Capitalize<T>}`

type HeavyName = Heavy<'linbudu'>; // "LINBUDU"
type RespectName = Respect<'linbudu'>; // "Linbudu"

// 小驼峰
type CopyWithRename<T extends object> = {
  [K in keyof T as `modified${Capitalize<string & K>}`]: T[K];
};


interface Foo {
  name: string;
  age: number;
}

type CopiedFoo = CopyWithRename<Foo>;

// 等价于
type CopiedFooEqual = {
  modifiedName: string;
  modifiedAge: number;
}

8.5.5 结合模式匹配

模板插槽不仅可以声明一个占位,也可以声明一个要提取的部分,使用模板插槽 + infer 关键字提取:

type ReverseName<T extends string> = T extends `${infer start} ${infer end}` 
    ? `${Capitalize<end & string>} ${start}`
    : T

type ReversedTomHardy = ReverseName<'Tom hardy'>; // "Hardy Tom"
type ReversedLinbudu = ReverseName<'Budu Lin'>; // "Lin Budu"

注意,这里的空格也需要严格遵循,因为它也是一个字面量类型的一部分

九、协变与逆变

随着某一个量的变化,随之变化一致的即称为协变,而变化相反的即称为逆变。

用 TypeScript 的思路进行转换,即如果有 A ≼ B ,协变意味着 Wrapper<A> ≼ Wrapper<B>,而逆变意味着 Wrapper<B> ≼ Wrapper<A>变化(Wrapper)即指从单个类型到函数类型的包装过程。

演示例子:

class Animal {
  asPet() {}
}

class Dog extends Animal {
  bark() {}
}

class Corgi extends Dog {
  cute() {}
}

// 函数参数类型
type AsFuncArgType<T> = (arg: T) => void;

// 函数返回值类型
type AsFuncReturnType<T> = (arg: unknown) => T;

// 成立:函数参数类型使用逆变(Dog -> T) ≼ (Animal -> T) 返回1
type CheckArgType1 = AsFuncArgType<Animal> extends AsFuncArgType<Dog> ? 1 : 2;

// 不成立:(Dog -> T) ≼ (Animal -> T) 返回2
type CheckArgType = AsFuncArgType<Dog> extends AsFuncArgType<Animal> ? 1 : 2;

// 函数返回值遵循协变 成立:(T -> Corgi) ≼ (T -> Dog) 返回1
type CheckReturnType = AsFuncReturnType<Corgi> extends AsFuncReturnType<Dog>
  ? 1
  : 2;

总结:函数类型的参数类型使用子类型逆变的方式确定是否成立,而返回值类型使用子类型协变的方式确定。。通俗点来说,对于如何对两个函数类型进行兼容性比较这一问题,则是比较它们的参数类型是否是反向父子类型关系,返回值是否是正向父子类型关系。

默认情况下,对函数参数的检查采用 双变(  bivariant   ,即逆变与协变都被认为是可接受的。如果开始了 strictFunctionTypes ,即是 对函数参数类型启用逆变检查,在比较两个函数类型是否兼容时,将对函数参数进行更严格的检查。

在上面我们只关注了显式的父子类型关系,实际上在类型层级中还有隐式的父子类型关系(联合类型)以及兄弟类型(同一基类的两个派生类)。对于隐式的父子类型其可以仍然沿用 显式的父子类型协变与逆变判断,但对于兄弟类型,比如 Dog 与 Cat,需要注意的是它们根本就不满足逆变与协变的发生条件(父子类型) ,因此 (Cat -> void) ≼ (Dog -> void) (或者反过来)无论在严格检查与默认情况下均不成立

十、内置工具类型进阶

10.1、属性修饰进阶

在内置工具类型中,对属性修饰工具类型的进阶主要分为这么几个方向:

  • 深层的属性修饰;
  • 基于已知属性的部分修饰,以及基于属性类型的部分修饰

10.1.1 深层的属性修饰-递归

首先,可以利用 infer 关键字得到一个递归工具类型:

type PromiseValue<T> = T extends Promise<infer V> ? PromiseValue<V> : T;

即:在条件类型成立时,再次调用了这个工具类型。在某一次递归到条件类型不成立时,就会直接返回这个类型值。

也可以对 Partial、Required这样子处理:

type Partial<T> = {
  [P in keyof T]?: T[P];
};
// 递归
type DeepPartial<T extends object> = {
  [K in keyof T]?: T[K] extends object ? DeepPartial<T[K]> : T[K]
}

type Required<T> = {
  [P in keyof T]-?: T[P];
};
// 递归
type DeepRequired<T extends object> = {
  [K in keyof T]-?: T[K] extends object ? DeepRequired<T[K]> : T[K]
}

type Readonly<T> = {
  readonly [P in keyof T]: T[P];
};
// 递归
type DeepReadonly<T extends object> = {
  readonly [P in keyof T]: T[P] extends object ? DeepReadonly<T[P]> : T[P]
}

// 递归
type DeepMutable<T extends object> = {
  -readonly [K in keyof T]: T[K] extends object ? DeepMutable<T[K]> : T[K];
};

type NonNullable<T> = T extends null | undefined ? never : T;

// 递归
type DeepNonNullable<T extends object> = {
  [K in keyof T]: T[K] extends object ? DeepNonNullable<T[K]> : NonNullable<T[K]>
}

export type Nullable<T> = T | null;
// 递归
export type DeepNullable<T extends object> = {
  [K in keyof T]: T[K] extends object ? DeepNullable<T[K]> : Nullable<T[K]>;
};

需要注意的是,DeepNullable 和 DeepNonNullable 需要在开启 --strictNullChecks 下才能正常工作。

10.1.2 对已知属性进行部分修饰

对已知属性进行部分修饰的编程思路:将复杂的工具类型,拆解为由基础工具类型、类型工具的组合

比如:要让一个对象的三个已知属性为可选的,那只要把这个对象拆成 A、B 两个对象结构,分别由三个属性和其他属性组成。然后让对象 A 的属性全部变为可选的,和另外一个对象 B 组合起来。

直接来看基于已知属性的部分修饰,MarkPropsAsOptional 会将一个对象的部分属性标记为可选:

type MarkPropsAsOptional<
  T extends object,
  K extends keyof T = keyof T
> = Partial<Pick<T, K>> & Omit<T, K>;

分析:

  • T 为需要处理的对象类型,而 K 为需要标记为可选的属性;
  • K 必须为 T 内部的属性,因此我们将其约束为 keyof T,即对象属性组成的字面量联合类型;
  • K指定默认值也为 keyof T,这样在不传入第二个泛型参数时,它的表现就和 Partial 一致,即全量的属性可选;
  • Pick<T, K>是选取对应的属性组成子结构;
  • Partial<Pick<T, K>>将选出的属性标记为可选;
  • Omit<T, K>筛选出除需要标记为可选属性以外的其他属性组成子结构;
  • 最后使用交叉类型将其组合
type MarkPropsAsOptionalStruct = MarkPropsAsOptional<
  {
    foo: string;
    bar: number;
    baz: boolean;
  },
  'bar'
>;

但是这样子得处理的结果看不出具体效果,如图所示:

TypeScript:入门与进阶(下)

可以引入一个辅助的工具类型(Flatten),对于这种交叉类型的结构,Flatten 能够将它展平为单层的对象结构:

type Flatten<T> = { [K in keyof T]: T[K] };

type FlattenMarkPropsAsOptional<
    T extends object,
    K extends keyof T = keyof T
 > = Flatten<MarkPropsAsOptional<T, K>>

type MarkPropsAsOptionalStruct = FlattenMarkPropsAsOptional<
  {
    foo: string;
    bar: number;
    baz: boolean;
  },
  'bar'
>;

最后的效果如图所示:

TypeScript:入门与进阶(下)

简化后的代码:

type Flatten<T> = { [K in keyof T]: T[K] };

type MarkPropsAsOptional<
  T extends object,
  K extends keyof T = keyof T
> = Flatten<Partial<Pick<T, K>> & Omit<T, K>>;

type MarkPropsAsOptionalStruct = MarkPropsAsOptional<
  {
    foo: string;
    bar: number;
    baz: boolean;
  },
  'bar'
>;

实现其它类型的部分修饰

type Flatten<T> = { [K in keyof T]: T[K] };

type MarkPropsAsRequired<
  T extends object,
  K extends keyof T = keyof T
> = Flatten<Omit<T, K> & Required<Pick<T, K>>>;

type MarkPropsAsReadonly<
  T extends object,
  K extends keyof T = keyof T
> = Flatten<Omit<T, K> & Readonly<Pick<T, K>>>;


type Mutable<T> = {
  -readonly [P in keyof T]: T[P];
};

type MarkPropsAsMutable<
  T extends object,
  K extends keyof T = keyof T
> = Flatten<Omit<T, K> & Mutable<Pick<T, K>>>;


type Nullable<T> = T | null;

type MarkPropsAsNullable<
  T extends object,
  K extends keyof T = keyof T
> = Flatten<Omit<T, K> & Nullable<Pick<T, K>>>;

// NonNullable也是内置工具类型
// type NonNullable<T> = T extends null | undefined ? never : T;

type MarkPropsAsNonNullable<
  T extends object,
  K extends keyof T = keyof T
> = Flatten<Omit<T, K> & NonNullable<Pick<T, K>>>;

10.2 结构工具类型进阶

对结构工具类型主要给出了两个进阶方向:

  • 基于键值类型的 PickOmit
  • 子结构的互斥处理;

10.2.1 PickByValueType

首先是基于键值类型的 Pick 与 Omit,我们就称之为PickByValueType,它是基于期望的类型去拿到所有此类型的属性名组成的类型结构,再结合键值类型PickOmit实现的。它的实现方式其实还是类似部分属性修饰中那样,将对象拆分为两个部分,处理完毕再组装。

具体实现思路:

  • 第一步:现在我们无法预先确定要拆分的属性了,而是需要基于期望的类型去拿到所有此类型的属性名(实现ExpectedPropKeys方法)。
  • 第二步:拿到对应类型的属性名后,将这些属性交给 PickOmit,得到由属性组成的子结构。

比如:想 Pick 出所有函数类型的值,那就要先拿到所有的函数类型属性名。

type FuncStruct = (...args: any[]) => any;

// 筛选出键值是符合条件的函数的属性名
type Tmp<T extends object> = {
  [K in keyof T]: T[K] extends FuncStruct ? K : never;
};

type Res = Tmp<{
  foo: () => void;
  bar: () => number;
  baz: number;
}>;

// Res等价于
type ResEqual = {
  foo: 'foo';
  bar: 'bar';
  baz: never;
};

 //never在联合类型中会被直接移除 所以类型是: "foo" | "bar" 
type WhatWillWeGet = Res[keyof Res];

此时,已经拿到了所有的函数类型属性名的联合类型的: TypeScript:入门与进阶(下)

简化后的代码:

type FuncStruct = (...args: any[]) => any;

type FunctionKeys<T extends object> = {
  [K in keyof T]: T[K] extends FuncStruct ? K : never
}[keyof T]

补充说明:

  • T[K] extends FuncStruct ? K : never :如果条件成立,返回的是属性名;
  • { [K in keyof T]: T[K] extends FuncStruct ? K : never; } :如果条件成立,返回的是属性名-属性名 字面量类型,如果不成立则返回 属性名-never
  • type FunctionKeys<T extends object> = { [K in keyof T]: T[K] extends FuncStruct ? K : never}:返回的就是符合条件的 属性名-属性名 或 属性名-never 组成的类型结构;
  • {}[keyof T] :也就是索引类型查询 + keyof 操作符的组合,得到的是一组由属性名组成的联合类型

通过这一方式,我们就能够获取到符合预期类型的属性名了。如果希望抽象“基于键值类型查找属性”名这么个逻辑,我们就需要对 FunctionKeys 的逻辑进行封装,即将预期类型也作为泛型参数,由外部传入:

// 基于键值类型查找属性名
type ExpectedPropKeys<T extends object, ValueType> = {
  [Key in keyof T]-?: T[Key] extends ValueType ? Key : never;
}[keyof T];

注意,为了避免可选属性对条件类型语句造成干扰,这里我们使用 -? 移除了所有可选标记

实际业务中使用:

type FuncStruct = (...args: any[]) => any;

type FunctionKeys<T extends object> = ExpectedPropKeys<T, FuncStruct>;

// "foo"|"bar"  如果没有排除可选属性,结果是:"foo"|"bar"|undefined
type Res = FunctionKeys<{
  foo: () => void;
  bar: () => number;
  getName?: () => number
  baz: number;
}>

拿到对应类型的属性名后,将这些属性交给 Pick ,就可以得到由这些属性组成的子结构了:

// 根据属性名,选取对应的键值
type PickByValueType<T extends object, ValueType> = Pick<
  T,
  ExpectedPropKeys<T, ValueType>
>;

type res1 = PickByValueType<{ foo: string; bar: number, name: string }, string>

type res1Equal = {
  foo: string;
  name: string
}

当然,也可以结合映射类型-in实现:

type PickByValueType< T extends object, ValueType> = {
  [k in ExpectedPropKeys<T, ValueType>]: ValueType
}

到这里,基于期望的类型去拿到所有此类型的属性名组成的类型结构已经实现:

// 基于键值类型查找属性名
type ExpectedPropKeys<T extends object, ValueType> = {
  [Key in keyof T]-?: T[Key] extends ValueType ? Key : never;
}[keyof T];

// 根据属性名,选取对应的键值
type PickByValueType<T extends object, ValueType> = Pick<
  T,
  ExpectedPropKeys<T, ValueType>
>;

10.2.2 OmitByValueType

OmitByValueType其实就是基于期望的类型去拿到所有此类型的属性名后,排除期望的类型组成的类型结构,与 PickByValueType 实现思路类似,有两种方案:

1. 方案一: 实现一个和ExpectedPropKeys 作用相反的工具类型FilteredPropKeys,也就是只需要调换条件类型语句结果的两端,再结合 Pick选取对应的类型结构:

type FilteredPropKeys<T extends object, ValueType> = {
  [Key in keyof T]-?: T[Key] extends ValueType ? never : Key;
}[keyof T];

type OmitByValueType<T extends object, ValueType> = Pick<
  T,
  FilteredPropKeys<T, ValueType>
>;

type Res = OmitByValueType<{ foo: string; bar: number; baz: boolean }, string | number>

// 等价于
type ResEqual = {
  baz: boolean
}

2. 方案二: 通过 ExpectedPropKeys获取到属性名后,结合Omit实现:

// 根据对应的类型,获取属性名
type ExpectedPropKeys<T extends object, ValueType> = {
  [Key in keyof T]-?: T[Key] extends ValueType ? Key : never;
}[keyof T];

type OmitByValueType<T extends object, ValueType> = Omit<
  T,
  ExpectedPropKeys<T, ValueType>
>;

type Res = OmitByValueType<{ foo: string; bar: number; baz: boolean }, string | number>

10.2.3 合并ExpectedPropKeys和FilteredPropKeys

如果想把 ExpectedPropKeysFilteredPropKeys 合并在一起,只是需要引入第三个泛型参数来控制返回结果:

type Conditional<Value, Condition, Resolved, Rejected> = Value extends Condition ? Resolved : Rejected;

// 第三个参数Positive是个泛型
type ValueTypeFilter<
  T extends object,
  ValueType,
  Positive extends boolean
> = {
  [Key in keyof T]: T[Key] extends ValueType
    ? Conditional<Positive, true, Key, never>
    : Conditional<Positive, true, never, Key>
}[keyof T]

type PickByValueType<T extends object, ValueType> = Pick<
  T,
  ValueTypeFilter<T, ValueType, true>
>

type OmitByValueType<T extends object, ValueType> = Pick<
  T,
  ValueTypeFilter<T, ValueType, false>
>

type ResPick = PickByValueType<{ foo: string; bar: number; baz: boolean }, string | number>

// 等价于
type ResPickEqual = {
  foo: string;
  bar: number;
}

type ResOmit = OmitByValueType<{ foo: string; bar: number; baz: boolean }, string | number>

// 等价于
type ResOmitEqual = {
  baz: boolean
}

但这里基于条件类型的比较存在特殊情况,即在联合类型的情况下,1 | 2 extends 1 | 2 | 3(通过泛型参数传入) 会被视为是合法的,这是由于分布式条件类型的存在。而有时我们希望对联合类型的比较是全等的比较,则需要禁用分布式条件类型:

type Wrapped<T> = [T] extends [boolean] ? "Y" : "N";

type StrictConditional<Value, Condition, Resolved, Rejected> = [Value] extends [Condition]
  ? Resolved
  : Rejected;

type Res1 = StrictConditional<1 | 2, 1 | 2 | 3, true, false>; // true

当条件不再是一个简单的单体类型,而是一个联合类型时,我们使用数组的方式就产生问题了。因为 Array<1 | 2> extends Array<1 | 2 | 3> 就是合法的,第一个数组中的可能元素类型均被第二个数组的元素类型包含了,无论如何都是其子类型

那么现在应该怎么办?其实只要反过来看,既然 Array<1 | 2> extends Array<1 | 2 | 3> 成立,那么 Array<1 | 2 | 3> extends Array<1 | 2> 肯定是不成立的,我们只要再加一个反方向的比较即可:

type StrictConditional<
  A,
  B,
  Resolved,
  Rejected,
  Fallback = never
> = [A] extends [B]
      ? [B] extends [A]
        ? Resolved
        : Rejected
      : Fallback;

type Res1 = StrictConditional<1 | 2, 1 | 2 | 3, true, false>; // false
type Res2 = StrictConditional<1 | 2 | 3, 1 | 2, true, false, false>; // false
type Res4 = StrictConditional<1 | 2 | 3, 1 | 2, true, false>; // never

type Res3 = StrictConditional<1 | 2, 1 | 2, true, false>; // true

应用到 TypeFilter 中:

// 基于期望的类型去拿到所有此类型的属性名
type StrictValueTypeFilter<
  T extends object,
  ValueType,
  Positive extends boolean = true
> = {
  [Key in keyof T]-?
    : StrictConditional<
        ValueType,
        T[Key],
        Positive extends true ? Key : never,// Resolved
        Positive extends true ? never : Key,// Rejected
        Positive extends true ? never : Key// Fallback
    >
}[keyof T]

// Pick出符合属性名的类型结构
type StrictPickByValueType<T extends object, ValueType> = Pick<
  T,
  StrictValueTypeFilter<T, ValueType>
>;


type Res1 = StrictPickByValueType<{ foo: 1; bar: 1 | 2; baz: 1 | 2 | 3 }, 1 | 2> // {bar:1|2}
type Res2 = StrictPickByValueType<{ foo: 1; bar: 1 | 2; baz: 1 | 2 | 3 }, 1 | 2 | 3> // {baz:1|2|3}

type Res3 = StrictPickByValueType<{ foo: 1; bar: 1 | 2; baz: 1 | 2 | 3 }, 1> // {for: 1}

StrictOmitByValueType 也类似:

export type StrictOmitByValueType<T extends object, ValueType> = Pick<
  T,
  StrictValueTypeFilter<T, ValueType, false>
>;

type Res4 = StrictOmitByValueType<{ foo: 1; bar: 1 | 2; baz: 1 | 2 | 3 }, 1 | 2> // { foo: 1; baz: 1 | 2 | 3; }
type Res5 = StrictOmitByValueType<{ foo: 1; bar: 1 | 2; baz: 1 | 2 | 3 }, 1 | 2 | 3> // { foo: 1; bar: 1 | 2; }

type Res6 = StrictOmitByValueType<{ foo: 1; bar: 1 | 2; baz: 1 | 2 | 3 }, 1> // { bar: 1 | 2; baz: 1 | 2 | 3 }

需要注意的是,由于 StrictOmitByValueType 需要的是不符合类型的属性,因此这里 StrictConditional 的 Fallback 泛型参数也需要传入 Key (即第五个参数中的 Positive extends true ? never : Key),同时整体应当基于 Pick 来实现。

10.2.4 拓展:RequiredKeys、OptionalKeys

在属性修饰工具类PickByValueType 我们实现了FunctionKeys

type FuncStruct = (...args: any[]) => any;

type FunctionKeys<T extends object> = {
  [K in keyof T]: T[K] extends FuncStruct ? K : never
}[keyof T]

如果,我们要获取一个接口中所有可选或必选的属性,现在没法通过类型判断,要怎么去收集属性? 首先是 RequiredKeys ,我们可以通过一个很巧妙的方式判断一个属性是否是必选的,先看一个例子:

type Tmp1 = {} extends { prop: number } ? "Y" : "N"; // "N"
type Tmp2 = {} extends { prop?: number } ? "Y" : "N"; // "Y"

根据 {} extends { prop?: number }返回 Y,可以视为 {}继承自 { prop?: number },因此可以利用这个特性实现RequiredKeysOptionalKeys

type RequiredKeys<T> = {
  [K in keyof T]-?: {} extends Pick<T, K> ? never : K;
}[keyof T];

type OptionalKeys<T> = {
  [K in keyof T]-?: {} extends Pick<T, K> ? K : never;
}[keyof T];

// "foo" | "bar"
type ResRequired = RequiredKeys<{
  foo: () => void;
  bar: () => number;
  baz?: number;
}>;

// "baz"
type ResOptional = OptionalKeys<{
  foo: () => void;
  bar: () => number;
  baz?: number;
}>;

10.3 集合工具类型进阶

在集合工具类型中我们给到的进阶方向,其实就是从一维原始类型集合,扩展二维的对象类型,在对象类型之间进行交并补差集的运算,以及对同名属性的各种处理情况。

对于对象类型的交并补差集,实现思路:

  • 先得到对象属性名集合的交并补差集;
  • 拿到对应类型的属性名后,利用PickOmit实现;

基础的实现:

// 并集
type Concurrence<A, B> = A | B;

// 交集
type Intersection<A, B> = A extends B ? A : never;

// 差集
type Difference<A, B> = A extends B ? never : A;

// 补集 B是A的子集
type Complement<A, B extends A> = Difference<A, B>;

实现对象属性名的版本:

// 对象类型描述结构
type PlainObjectType = Record<string, any>

// 属性名并集
type ObjectKeysConcurrence<
  T extends PlainObjectType,
  U extends PlainObjectType
> = Concurrence<keyof T, keyof U>;

// 属性名交集
type ObjectKeysIntersection<
T extends PlainObjectType,
U extends PlainObjectType
> = Intersection<keyof T, keyof U>

// 属性名差集
type ObjectKeysDifference<
T extends PlainObjectType,
U extends PlainObjectType
> = Difference<keyof T, keyof U>

// 属性名补集
type ObjectKeysComplement<
  T extends U,
  
  U extends PlainObjectType
> = Complement<keyof T, keyof U>;

对于交集、补集、差集,我们可以直接使用属性名的集合来实现对象层面的版本:

// 对象类型的交集
type ObjectIntersection<
  T extends PlainObjectType,
  U extends PlainObjectType
> = Pick<T, ObjectKeysIntersection<T, U>>;

// 对象类型的差集
type ObjectDifference<
  T extends PlainObjectType,
  U extends PlainObjectType
> = Pick<T, ObjectKeysDifference<T, U>>;

// 对象类型的补集
type ObjectComplement<T extends U, U extends PlainObjectType> = Pick<
  T,
  ObjectKeysComplement<T, U>
>;

需要注意的是在 ObjectKeysComplementObjectComplement 中,T extends U 意味着 TU 的子类型,但在属性组成的集合类型中却相反,U 的属性联合类型是 T 的属性联合类型的子类型,因为既然 TU 的子类型,那很显然 T 所拥有的的属性会更多。

而对于并集,就不能简单使用属性名并集版本了,因为使用联合类型实现,我们并不能控制同名属性的优先级,比如我到底是保持原对象属性类型呢,还是使用新对象属性类型?

10.4 模式匹配工具类型进阶

在内置工具类型一节中,我们对模式匹配工具类型的进阶方向其实只有深层嵌套这么一种,比如此前我们实现了提取函数的首个参数类型:

type FunctionType = (...args: any) => any;

type FirstParameter<T extends FunctionType> = T extends (
    arg: infer P,
    ...args: any
  ) => any
  ? P
  : never

要提取最后一个参数类型:

type FunctionType = (...args: any) => any;

type LastParameter<T extends FunctionType> = T extends (
  arg: infer P
  ) => any
  ? P
  : T extends (...args: infer R) => any
    ? R extends [...any, infer Q]
      ? Q
      : never
  : never

type FuncFoo = (arg: number) => void;
type FuncBar = (...args: string[]) => void;
type FuncBaz = (arg1: string, arg2: boolean) => void;

type FooLastParameter = LastParameter<FuncFoo>; // number
type BarLastParameter = LastParameter<FuncBar>; // string
type BazLastParameter = LastParameter<FuncBaz>; // boolean

10.5 模板字符串工具类型进阶

模板字符串在模板插槽中使用 infer 关键字:

type ReverseName<T extends string> = T extends `${infer start} ${infer end}` 
    ? `${Capitalize<end & string>} ${start}`
    : T

对模板字符串类型中使用模式匹配时,本质上就是在一个字符串字面量类型结构做处理。对比到字符串类型变量的方法,也就是 trim(trimLeft、trimRight)includesstartsWithendsWith

10.5.1 Include

字符串的includes方法的作用是判断传入的字符串字面量类型中是否含有某个字符串,对应的类型层面的实现:

type Include<
  Str extends string,
  search extends string
> = Str extends `${infer L}${search}${infer R}` ? true : false;

type IncludeRes1 = Include<'linbudu', 'lin'>; // true
type IncludeRes2 = Include<'linbudu', '_lin'>; // false
type IncludeRes3 = Include<'linbudu', ''>; // true
type IncludeRes4 = Include<' ', ''>; // true
type IncludeRes5 = Include<'', ''>; // false

Include实现了判断Str是否包含search,而条件语句是:

Str extends `${infer L}${search}${infer R}` ? true : false

由此可以看作是,Strsearch的子类型。根据类型系统层级分析,子类型能兼容父类型,而父类型不能兼容子类型。

IncludeRes5中,也应当是成立的,因为''.includes('')是成立的,所以需要做特殊处理:

type _Include<
  Str extends string,
  search extends string
> = Str extends `${infer L}${search}${infer R}` ? true : false;

type Include<
  Str extends string,
  search extends string
> = Str extends ''
      ? search extends ''
        ? true
        : false
      : _Include<Str, search>

type IncludeRes5 = Include<'', ''>; // true
type isEmptyString = '' extends '' ? true: false;//true

10.5.2 Trim系列

去除起始部分空格的 trimStart,去除结尾部分空格的 trimEnd,以及开头结尾空格一起去的 trim对应的类型实现:

// trimStart
type TrimLeft<Str extends string> = Str extends ` ${infer R}` ? R : Str;

// trimEnd
type TrimRight<Str extends string> = Str extends `${infer R} ` ? R : Str;

// trim
type Trim<Str extends string> = TrimLeft<TrimRight<Str>>;

type TempStr = ' abc '
type res1 = TrimLeft<TempStr>// "abc "
type res2 = TrimRight<TempStr> // " abc"
type res3 = Trim<TempStr> // "abc"

去除字符串里的所有空格,可以用递归:

type TrimLeft<Str extends string> = Str extends ` ${infer R}` ? TrimLeft<R> : Str;

type TrimRight<Str extends string> = Str extends `${infer R} ` ? TrimRight<R> : Str;

type Trim<Str extends string> = TrimLeft<TrimRight<Str>>;

10.5.3 StartsWith与EndsWith

类型版本的 StartsWithEndsWith 两个工具类型,和 Include 的实现非常接近,先看 StartsWith

type _StartsWith<
  Str extends string,
  Search extends string
> = Str extends `${Search}${infer R}` ? true : false;

type StartsWith<
  Str extends string,
  Search extends string
> = Str extends ""
      ? Search extends ""
        ? true
        : false
      : _StartsWith<Str, Search>

type StartsWithRes1 = StartsWith<'linbudu', 'lin'>; // true
type StartsWithRes2 = StartsWith<'linbudu', ''>; // true
type StartsWithRes3 = StartsWith<'linbudu', ' '>; // false
type StartsWithRes4 = StartsWith<'', ''>; // true
type StartsWithRes5 = StartsWith<' ', ''>; // true

EndsWith的类型实现:

type _EndsWith<
  Str extends string,
  Search extends string
> = Str extends `${infer R}${Search}` ? true : false;

type EndsWith<
  Str extends string,
  Search extends string
> = Str extends ""
      ? Search extends ""
        ? true
        : false
      : _EndsWith<Str, Search>

type EndsWithRes1 = EndsWith<'linbudu', 'du'>; // true
type EndsWithRes2 = EndsWith<'linbudu', ''>; // true
type EndsWithRes3 = EndsWith<'linbudu', ' '>; // false
type EndsWithRes4 = EndsWith<'', ''>; // true
type EndsWithRes5 = EndsWith<' ', ''>; // true

10.5.4 Replace、ReplaceAll

Replace 的作用是,将目标部分替换为新的部分,按照原本的结构组合好

type Replace<
  Str extends string,
  Search extends string,
  Replacement extends string
> = Str extends `${infer Head}${Search}${infer Tail}`
  ? `${Head}${Replacement}${Tail}`
  : Str;

// "管仓库的仓库员"
type ReplaceRes1 = Replace<'管理员', '理', '仓库的仓库'>;
// "不喜欢唠嗑的管理员"
type ReplaceRes3 = Replace<'爱唠嗑的管理员', '爱', '不喜欢'>;
// "管理大棚"
type ReplaceRes4 = Replace<'管理员', '员', '大棚'>;
// 不发生替换
type ReplaceRes2 = Replace<'管理员', '?', '??'>;

全量替换:

type ReplaceAll<
  Str extends string,
  Search extends string,
  Replacement extends string
> = Str extends `${infer Head}${Search}${infer Tail}`
  ? ReplaceAll<`${Head}${Replacement}${Tail}`, Search, Replacement>
  : Str;
  
// "mmm.linbudu.top"
type ReplaceAllRes1 = ReplaceAll<'www.linbudu.top', 'w', 'm'>;
// "www-linbudu-top"
type ReplaceAllRes2 = ReplaceAll<'www.linbudu.top', '.', '-'>;

10.5.5 Split、Join

split 的作用是,将字符串按照确定的分隔符拆分成一个数组

type Split<Str extends string> =
  Str extends `${infer Head}-${infer Body}-${infer Tail}`
    ? [Head, Body, Tail]
    : [];

type SplitRes1 = Split<'lin-bu-du'>; // ["lin", "bu", "du"]

如果分隔符与字符串长度不确定:

type Split<
  Str extends string,
  Delimiter extends string
> = Str extends `${infer Head}${Delimiter}${infer Tail}`// 判断是否兼容
  ? [Head, ...Split<Tail, Delimiter>]// 提取第一个分隔符的第一部分到数组,第二部进行递归
  : Str extends Delimiter// 如果不兼容,判断是否包含分隔符
    ? []
    : [Str];
    
// ["linbudu", "599", "fe"]
type SplitRes1 = Split<'linbudu,599,fe', ','>;

// ["linbudu", "599", "fe"]
type SplitRes2 = Split<'linbudu 599 fe', ' '>;

// ["l", "i", "n", "b", "u", "d", "u"]
type SplitRes3 = Split<'linbudu', ''>;    

高级语法

创建一个项目:在控制台执行npm init创建package.json文件, tsc --init创建tsconfig.json文件,执行npm install ts-node -D安装ts-node,执行npm install typescript --save安装typescript

新建src/index.ts页面,配置package.json文件,默认运行src/index.ts

"scripts": {
    "dev": "ts-node ./src/index.ts"
  },

执行npm run dev,验证装饰器的可行性:

function testDecorator(constructor: any) {
  console.log('decorator');
}

@testDecorator
class Test {};

let test = new Test();

会报如下错误: TypeScript:入门与进阶(下)

因为装饰器是试验性质的,所以tsconfig.json需要打开支持试验性质的api。

"experimentalDecorators": true, 
"emitDecoratorMetadata": true,

再次执行npm run dev,运行成功

TypeScript:入门与进阶(下)

装饰器

类的装饰器

类的装饰器的特点:

  • 装饰器本身是一个函数
  • 装饰器通过 @ 符号来使用
  • 装饰器在类创建时立即执行,而不是实例化时执行
    function testDecorator() {
      console.log('decorator');
    }
    
    @testDecorator
    class Test {};
    const test = new Test();
    const test1 = new Test();
    
    // 不需要实例化也可以执行输出
    // 只输出一次 decorator
    
    
  • 类装饰器接收的参数是构造函数
    // 参数是构造函数
    function testDecorator(constructor: any) {
      // console.log('decorator'); 
      constructor.prototype.getName = () => {
        console.log('dell');
      }
    }
    @testDecorator
    class Test {};
    
    let test = new Test();
    
    (test as any).getName()
    // 输出 dell
    
  • 类的装饰器是从下到上,从右到左执行
    function testDecorator(constructor: any) {
      console.log('decorator'); 
    }
    
    function testDecorator1(constructor: any) {
            console.log("decorator1")
    }
    
    @testDecorator
    @testDecorator1
    class Test {};
    
    // 也可以写在同一行
    // @testDecorator @testDecorator1
    // class Test {};
    
    let test = new Test();
    // 依次输出 decorator1  decorator
    
  • 类的装饰器可以利用工厂模式进行包装
    function testDecorator(flag:boolean) {
      if (flag) {
        return function(constructor: any) {
          constructor.prototype.getName = () => {
            console.log('dell');
          }
        }
      } else {
        return function(constructor:any) {}
      }
    
    }
    
    // 可以通过参数传值去控制修饰器
    @testDecorator(true)
    class Test {};
    
    let test = new Test();
    
    (test as any).getName()
    
    // 输出 dell
    // 如果装饰器传入false,则报错
    

扩展:

因为类装饰器接收的参数是构造函数,使用any类型则失去了ts的优势,这里讲一下装饰器的参数实际是一个class的构造函数

/**
 * 
 * (...args: any[]) => any
 * 这是一个函数,返回值是any,这个函数的有多个参数,将参数合并成一个any类型的数组
 * 
 * new (...args: any[]) => any
 * 因为前面有new  所以这是个构造函数,并且实例化了
 * 
 * 
 * T extends new (...args: any[]) => any
 * 说明 T可以由后面的构造函数被实例化出来
 */


function testDecorator<T extends new (...args: any[]) => any>(constructor: T){
  return class extends constructor {};
}

@testDecorator
class Test {
    name: string
    constructor(name: string) {
         this.name = name
    }
}

const test = new Test('dell')
console.log(test)

打印输出: TypeScript:入门与进阶(下)

装饰器的构造函数可以重新修改实例化后的值

function testDecorator<T extends new (...args: any[]) => any>(constructor: T){
  return class extends constructor {
    name = 'lee'
  };
}

@testDecorator
class Test {
	name: string
	constructor(name: string) {
    console.log(1);
		this.name = name
    console.log(2);
	}
}

const test = new Test('dell')
console.log(test)

TypeScript:入门与进阶(下)

增加方法

function testDecorator() {
    return function <T extends new (...args: any[]) => any>(constructor: T) {
        return class extends constructor {
            name = "lee"
            getName() {
                 return this.name
            }
        }
    }
}

const Test = testDecorator()(
    class {
        name: string
        constructor(name: string) {
             this.name = name
        }
    }
)

const test = new Test("dell")
console.log(test.getName())// lee

类的方法的装饰器

  • 普通方法,target 对应的是类的 prototype
  • 静态方法,target 对应的是类的构造函数
// 普通方法,target 对应的是类的 prototype
// 静态方法,target 对应的是类的构造函数
// key 是修饰器修饰的方法的名称
function getNameDecorator(target: any, key: string) {
  console.log(target, key);
}

class Test {
  name: string;
  constructor(name: string) {
    this.name = name;
  }
  // 静态方法
  //@getNameDecorator
  //static getName() {
  //  return '123'
  //}
  
  @getNameDecorator
  getName() {
    return this.name;
  }
}

const test = new Test('dell');
console.log(test.getName());// dell

// console.log(Test.getName())

访问器的装饰器

修饰器的参数描述

// descriptor 是对类的属性的描述信息
function visitDecorator(target: any, key: string,descriptor: PropertyDescriptor) {
    // 如果外部修改了name,则会报错,因为描述信息的writable为false,表示不可修改
    // descriptor.writable = false;
}

class Test {
    private _name: string
    constructor(name: string) {
       this._name = name
    }
    get name() {
        return this._name
    }
    @visitDecorator
    set name(name: string) {
        this._name = name
    }
}

const test = new Test("dell")
test.name = "dell lee"
console.log(test.name)// dell lee

属性的装饰器

类的属性修改器,修改的并不是实例上的属性, 而是原型上的属性

// 修改的并不是实例上的 name, 而是原型上的 name
function nameDecorator(target: any, key: string): any {
    target[key] = "lee"
}

// name 放在实例上
class Test {
    @nameDecorator
    name = "Dell"
}

const test = new Test()
console.log(test.name);// Dell
console.log((test as any).__proto__.name)// lee
console.log(Test.prototype.name);// lee

参数修饰器

/**
 * 
 * @param target 原型 -- 装饰器对象
 * @param method 参数装饰器所在的方法名
 * @param paramIndex 参数装饰器所在参数的位置
 */

function paramDecorator(target: any, method: string, paramIndex: number) {
    console.log(target, method, paramIndex) // 输出顺序: {} getInfo 1
}

class Test {
    getInfo(name: string, @paramDecorator age: number) {
        console.log(name, age)// Dell 30
    }
}

const test = new Test()
test.getInfo("Dell", 30)

装饰器使用小案例

假设定义一个class去取值一个定义为undefined的变量的属性的值,则会报错

const userInfo: any = undefined

class Test {
	getName() {
		return userInfo.name
	}
}

const test = new Test()
console.log(test.getName())

错误如下:TypeError: Cannot read property 'name' of undefined... ,就是我们平时遇到的没有规避的空值所遇到的常规错误,此时我们的处理方法是try/catch

const userInfo: any = undefined

class Test {
    getName() {
        try {
          return userInfo.name
        } catch (error) {
          console.log("userInfo.name 不存在")
        }

    }
}

const test = new Test()
console.log(test.getName())

这种处理方法不太灵活,使用装饰器则可以灵活的处理同类型的所有错误

const userInfo: any = undefined

function catchError(msg: string) {
  return function(target:any, key:string, descriptor: PropertyDescriptor) {
    console.log(descriptor, "descriptor")
    
    const fn = descriptor.value;
    // 重写
    descriptor.value = function() {
      try {
        fn()
      } catch (e)  {
        console.log(msg)
        
      }
    }
  }
}

class Test {
    @catchError("userInfo.name 不存在")
    getName() {
        return userInfo.name
    }
    @catchError("userInfo.age 不存在")
    getAge() {
        return userInfo.age
    }
}

const test = new Test()
console.log(test.getName())
console.log(test.getAge())

TypeScript:入门与进阶(下)

reflect-metadata

传送门

Reflect Metadata 是 ES7 的一个提案,它主要用来在声明的时候添加和读取元数据。TypeScript 在 1.5+ 的版本已经支持它,你只需要:

  • npm i reflect-metadata --save
  • 在 tsconfig.json 里配置 emitDecoratorMetadata 选项。

API

// define metadata on an object or property
// 定义
Reflect.defineMetadata(metadataKey, metadataValue, target);
Reflect.defineMetadata(metadataKey, metadataValue, target, propertyKey);

// check for presence of a metadata key on the prototype chain of an object or property
let result = Reflect.hasMetadata(metadataKey, target);
let result = Reflect.hasMetadata(metadataKey, target, propertyKey);

// check for presence of an own metadata key of an object or property
let result = Reflect.hasOwnMetadata(metadataKey, target);
let result = Reflect.hasOwnMetadata(metadataKey, target, propertyKey);

// get metadata value of a metadata key on the prototype chain of an object or property
let result = Reflect.getMetadata(metadataKey, target);
let result = Reflect.getMetadata(metadataKey, target, propertyKey);

// get metadata value of an own metadata key of an object or property
let result = Reflect.getOwnMetadata(metadataKey, target);
let result = Reflect.getOwnMetadata(metadataKey, target, propertyKey);

// get all metadata keys on the prototype chain of an object or property
let result = Reflect.getMetadataKeys(target);
let result = Reflect.getMetadataKeys(target, propertyKey);

// get all own metadata keys of an object or property
let result = Reflect.getOwnMetadataKeys(target);
let result = Reflect.getOwnMetadataKeys(target, propertyKey);

// delete metadata from an object or property
let result = Reflect.deleteMetadata(metadataKey, target);
let result = Reflect.deleteMetadata(metadataKey, target, propertyKey);

// apply metadata via a decorator to a constructor
@Reflect.metadata(metadataKey, metadataValue)
class C {
  // apply metadata via a decorator to a method (property)
  @Reflect.metadata(metadataKey, metadataValue)
  method() {
  }
}

定义与读取(defineMetadata、getMetadata)

import "reflect-metadata";
// 添加在对象上
const user = {
  name: 'dell'
}
// 定义
Reflect.defineMetadata('data', 'test', user)

console.log(user, "user")
// 读取
console.log(Reflect.getMetadata('data', user))// test


// 添加在类的属性上
class Teacher {
    @Reflect.metadata("data", "test")
    name = "dell"
}

console.log(Reflect.getMetadata('data', Teacher));// undefined
console.log(Reflect.getMetadata('data', Teacher.prototype));// undefined
// 读取  是类的原型上的,并且添加在name的属性上
console.log(Reflect.getMetadata('data', Teacher.prototype, 'name'));// test

检测键(key)是否存在(hasMetadata、hasOwnMetadata

import "reflect-metadata";
class User {
	@Reflect.metadata("data", "test")
	@Reflect.metadata("data1", "test")
	getName() {}
}

class Teacher extends User {}
// 可以查询继承的key
console.log(Reflect.hasMetadata("data", User.prototype, "getName"))// true
console.log(Reflect.hasMetadata("data", Teacher.prototype, "getName"))// true
// 继承的key无法查询
console.log(Reflect.hasOwnMetadata("data", User.prototype, "getName"))// true
console.log(Reflect.hasOwnMetadata("data", Teacher.prototype, "getName"))// false

获取reflect-metadata的键(key)

返回值是一个数组

import "reflect-metadata";
class User {
    @Reflect.metadata("data", "test")
    @Reflect.metadata("data1", "test")
    getName() {}
}

class Teacher extends User {}
// 获取所有的key
console.log(Reflect.getOwnMetadataKeys(User.prototype, "getName"))
console.log(Reflect.getOwnMetadataKeys(Teacher.prototype, "getName"))
// 还有 getMetadataKeys ,可以查询继承到的key

查询结果: TypeScript:入门与进阶(下)

装饰器的执行顺序

  • 类的方法的装饰器优先于 的装饰器执行
  • 验证时需要将tsconfig.json的target修改成"target": "es3"
import "reflect-metadata"

// 类的装饰器

/**
 *
 *  typeof User 获取User的类型
 *
 * let user = {name: 'lee', age: 1, talk() {}};
 * type User = typeof user; // 相当于 {name: string, age: number, talk():void}
 *
 */
function showData(target: typeof User) {
    for (let key in target.prototype) {
        const data = Reflect.getMetadata("data", target.prototype, key)
        console.log(data)// name age
        // 此处依然能打印出 name age ,说明方法的装饰会优先执行,从而在类的装饰器中能获取到
    }
}

// 方法装饰器
function setData(metadataKey: string, metadataValue: string) {
    return function (target: User, propertyKey: string) {
        // console.log(propertyKey, "propertyKey") // getName
        Reflect.defineMetadata(metadataKey, metadataValue, target, propertyKey)
    }
}

@showData
class User {
    @setData("data", "name")
    getName() {}

    @setData("data", "age")
    getAge() {}
}
转载自:https://juejin.cn/post/7181367051422793784
评论
请登录