当前位置: 首页>编程语言>正文

typescript bigint 原理 typescript for in

遇到难以理解的一定要手敲一边 创建 使用

in 和 extends 疑问

什么是类型编程

从一个简简单单的interface,到看起来挺高级的T extends SomeType ,再到各种不明觉厉的工具类型Partial、Required等,这些都属于类型编程的范畴。

正文

1.类型编程的基础:泛型

之所以上来就放泛型,是因为在 TypeScript 的整个类型编程体系中,它是最基础的那部分,所有的进阶类型都基于它书写。就像编程时我们不能没有变量,类型编程中的变量就是泛型

1.1基本写法
1.1.1Function中的写法参数和返回值相同

例子

function foo<T>(arg: T): T {
  return arg;
}

优点与规范

1.1.1泛型使得代码段的类型定义易于重用提升了灵活性与严谨性
1.1.2通常泛型。如 T U K V S等。项目达到一定复杂度后,使用带有具体意义的泛型变量声明,如 BasicBusinessType 这种形式。

1.1.2箭头函数中的写法
//第一种
const foo = <T>(arg:T) => arg
foo<string>("1")

//第二种
const templateTypeArr: <T>(pams: T) => T =params=> {
        return params;
    }
templateTypeArr<string>("1")
    
//在 TSX 文件中这么写,<T>可能会被识别为 JSX 标签,因此需要显式告知编译器
const foo = <T extends SomeBasicType>(arg: T) => arg;
1.1.3类中的写法
class Foo<T, U> {
  constructor(public arg1: T, public arg2: U) {}

  public method(): T {
    return this.arg1;
  }
}
new Foo<string,number>("1",1)
类型守卫、is in关键字
is关键字使用
/**
 * undefind、null、NaN、空数组、空字符串
 * @param {*} value - 数据
 * @returns {boolean}
 */
export function isEmpty(value: any): value is undefined | null | typeof NaN | [] | '';
if (isEmpty([])) console.log("是空的")
in关键字使用
class A {
  public a() {}

  public useA() {
    return "A";
  }
}

class B {
  public b() {}

  public useB() {
    return "B";
  }
}
//它能够判断一个属性是否为对象所拥有:
function useIt(arg: A | B): void {
  'a' in arg ? arg.useA() : arg.useB();
}
索引类型与映射类型
1. 索引类型
例子
1.1基本写法
// 假设key是obj键名
function pickSingleValue(obj, key) {
  return obj[key];
}

function pickSingleValue<T>(obj: T, key: keyof T): T[keyof T] {
  return obj[key];
}

keyof 是 索引类型查询 的语法, 它会返回后面跟着的类型参数的键值组成的字面量联合类型,

interface foo {
  a: number;
  b: string;
}
type A = keyof foo; // "a" | "b"
type PropAType = T["a"]; // number
1.2进阶写法

可以改进的地方
keyof出现了两次,以及泛型 T 其实应该被限制为对象类型。对于第一点,就像我们平时编程会做的那样:用一个变量把多处出现的存起来,记得,在类型编程里,泛型就是变量。

function pickSingleValue<T extends object, U extends keyof T>(
  obj: T,
  key: U
): T[U] {
  return obj[key];
}

extends
T extends object 理解为T 被限制为对象类型
U extends keyof T理解为 泛型 U 必然是泛型 T 的键名组成的联合类型(以字面量类型的形式,比如T这个对象的键名包括a b c,那么U的取值只能是"a" “b” "c"之一,即 “a” | “b” | “c”)
具体细节我们会在 条件类型 一章讲到。

上个例子的扩展

我们要取出一系列值,即参数 2 将是一个数组,成员均为参数 1 的键名组成:

function pick<T extends object, U extends keyof T>(obj: T, keys: U[]): T[U][] {
  return keys.map((key) => obj[key]);
}

// pick(obj, ['a', 'b'])

keys: U[] 我们知道 U 是 T 的键名组成的联合类型,那么要表示一个内部元素均是 T 键名的数组,就可以使用这种方式,具体的原理请参见下文的 分布式条件类型 章节

T[U][] 它的原理实际上和上面一条相同,首先是T[U],代表参数1的键值(就像Object[Key]),我认为它是一个很好地例子,表现了 TS 类型编程的组合性,你不感觉这种写法就像搭积木一样吗?

索引签名 Index Signature

在JavaScript中,我们通常使用 arr[1] 的方式索引数组,使用 obj[key] 的方式索引对象。说白了,索引就是你获取一个对象成员的方式,而在类型编程中,索引签名用于快速建立一个内部字段类型相同的接口,如

interface Foo {
  [keys: string]: string;
}

值得注意的是,由于 JS 可以同时通过数字与字符串访问对象属性,因此keyof Foo的结果会是string | numbe

//js代码
const o: Foo = {
 1: "芜湖!",
};

o[1] === o["1"]; // true

但是一旦某个接口的索引签名类型为number,那么使用它的对象就不能再通过字符串索引访问,如o[‘1’],将会抛出错误, 元素隐式具有 “any” 类型,因为索引表达式的类型不为 “number”。

映射类型 Mapped Types

在开始映射类型前,首先想想 JavaScript 中数组的 map 方法,通过使用map,我们从一个数组按照既定的映射关系获得一个新的数组。在类型编程中,我们则会从一个类型定义(包括但不限于接口、类型别名)映射得到一个新的类型定义。通常会在旧有类型的基础上进行改造,如:

例子

interface A {
  a: boolean;
  b: string;
  c: number;
  d: () => void;
}

现在我们有个需求,实现一个接口,它的字段与接口 A 完全相同,但是其中的类型全部为 string

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

重要的就是这个in操作符,你完全可以把它理解为 for…in/for…of 这种遍历的思路,获取到键名之后,键值就简单了,所以我们可以很容易的拷贝一个新的类型别名出来。

不是变量(泛型)是个特定属性名就会判断存不存在
是变量(泛型)可以把它理解为 for…in/

条件类型 Conditional Types

条件类型 的语法,实际上就是三元表达式

T extends U ? X : Y
//如果你觉得这里的 extends 不太好理解,可以暂时简单理解为 U 中的属性在 T 中都有。

类比到编程语句中,其实就是根据条件判断来动态的赋予变量值:

let unknownVar: string;

unknownVar = condition ? "淘系前端" : "淘宝FED";

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

又是一个例子使用条件类型作为函数返回值类型的例子

declare function strOrNum<T extends boolean>(
  x: T
): T extends true ? string : number;

在这种情况下,条件类型的推导就会被延迟,因为此时类型系统没有足够的信息来完成判断。只有给出了所需信息(在这里是入参 x 的类型),才可以完成推导。

const strReturnType = strOrNum(true);
const numReturnType = strOrNum(false);

就像三元表达式可以嵌套,条件类型也可以嵌套

type TypeName<T> = T extends string
  ? "string"
  : T extends number
  ? "number"
  : T extends boolean
  ? "boolean"
  : T extends undefined
  ? "undefined"
  : T extends Function
  ? "function"
  : "object";
分布式条件类型 Distributive Conditional Types

分布式条件类型实际上不是一种特殊的条件类型,而是其特性之一(所以说条件类型的分布式特性更为准确)。我们直接先上概念: 对于属于裸类型参数的检查类型,条件类型会在实例化时期自动分发到联合类型上

原文:
Conditional types in which the checked type is a naked type parameter are called distributive conditional types. Distributive conditional types are automatically distributed over union types during instantiation

  • 裸类型参数(就是泛型就是类型参数)
  • 实例化
  • 分发到联合类型
使用上面的TypeName类型别名
// 使用上面的TypeName类型别名
// "string" | "function"
type T1 = TypeName<string | (() => void)>;
// "string" | "object"
type T2 = TypeName<string | string[]>;
// "object"
type T3 = TypeName<string[] | number[]>;

上面的例子里,条件类型的推导结果都是联合类型(T3 实际上也是,只不过因为结果相同所以被合并了)
其实就是类型参数被依次进行条件判断后,再使用|组合得来的结果。

上面的例子中泛型都是裸露着的,如果被包裹着,其条件类型判断结果会有什么变化吗

还是例子
type Naked<T> = T extends boolean ? "Y" : "N";
type Wrapped<T> = [T] extends [boolean] ? "Y" : "N";

// "N" | "Y"
type Distributed = Naked<number | boolean>;

// "N"
type NotDistributed = Wrapped<number | boolean>;
  • 其中,Distributed类型别名,其类型参数(number | boolean)会正确的分发,即
    先分发到 Naked | Naked,再进行判断,所以结果是"N" | “Y”。
  • 而 NotDistributed 类型别名,第一眼看上去感觉TS应该会自动按数组进行分发,结果应该也是 “N” | “Y” ?但实际上,它的类型参数(number | boolean)不会有分发流程,直接进行[number | boolean] extends [boolean]的判断,所以结果是"N"。
几个概念
  1. 裸类型参数,没有额外被[]包裹过的,就像被数组包裹后就不能再被称为裸类型参数。
  2. 实例化,其实就是条件类型的判断过程,就像我们前面说的,条件类型需要在收集到足够的推断信息之后才能进行这个过程。在这里两个例子的实例化过程实际上是不同的,具体会在下一点中介绍。
  3. 分发到联合类型:
  4. 对于 TypeName,它内部的类型参数 T 是没有被包裹过的,所以 TypeName<string | (() => void)> 会被分发为 TypeName | TypeName<(() => void)>,然后再次进行判断,最后分发为"string" | “function”。
( A | B | C ) extends T ? X : Y
// 相当于
(A extends T ? X : Y) | (B extends T ? X : Y) | (B extends T ? X : Y)

// 使用[]包裹后,不会进行额外的分发逻辑。
[A | B | C] extends [T] ? X : Y

没有被 [] 额外包装的联合类型参数,在条件类型进行判定时会将联合类型分发,分别进行判断。

这两种行为没有好坏之分,区别只在于是否进行联合类型的分发,如果你需要走分布式条件类型,那么注意保持你的类型参数为裸类型参数。如果你想避免这种行为,那么使用 [] 包裹你的类型参数即可(注意在 extends 关键字的两侧都需要)。

infer关键字
在条件类型中,我们展示了如何通过条件判断来延迟确定类型,但仅仅使用条件类型也有一定不足:
它无法从条件上得到类型信息。举例来说,T extends Array<PrimitiveType> ? "foo" : "bar"
这一例子,我们不能从作为条件的 Array<PrimitiveType> 中获取到 PrimitiveType 的实际类型。

而这样的场景又是十分常见的,如获取函数返回值的类型、拆箱Promise / 数组等,因此这一节我们来介绍下 infer 关键字。

看一个简单的例子,用于获取函数返回值类型的工具类型ReturnType

const foo = (): string => {
  return "linbudu";
};

type ReturnType<T> = T extends (...args: any[]) => infer R ? R : never;

// string
type FooReturnType = ReturnType<typeof foo>;
  1. (...args: any[]) => infer R 是一个整体,这里函数的返回值类型的位置被 infer R 占据了。
  2. 当 ReturnType 被调用,类型参数 T 、R 被显式赋值(T为 typeof foo,infer R被整体赋值为string,即函数的返回值类型),如果 T 满足条件类型的约束,就返回nfer 完毕的R 的值,在这里 R 即为函数的返回值实际类型。
  3. 严谨写法
// 第一个 extends 约束可传入的泛型只能为函数类型
// 第二个 extends 作为条件判断
type ReturnType<T extends (...args: any[]) => any> = T extends (...args: any[]) => infer R ? R : never;
工具类型 Tool Type
type Partial<T> = {
  [K in keyof T]?: T[k];
};

它用于将一个接口中的字段全部变为可选,除了索引类型以及映射类型以外,它只使用了?可选修饰符,那么我现在直接掏出小抄:

  1. 去除可选修饰符:-?,位置与 ? 一致
  2. 只读修饰符:readonly,位置在键名,如 readonly key: string
  3. 去除只读修饰符:-readonly,位置同readonly
type Required<T> = {
  [K in keyof T]-?: T[K];
};

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

在上面我们实现了一个pick函数

function pick<T extends object, U extends keyof T>(obj: T, keys: U[]): T[U][] {
  return keys.map((key) => obj[key]);
}

类似的,假设我们现在需要从一个接口中挑选一些字段:

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

// 期望用法
// 期望结果 A["a"]类型 | A["b"]类型
type Part = Pick<A, "a" | "b">;

还是映射类型,只不过现在映射类型的映射源是传入给 Pick 的类型参数K。

既然有了Pick,那么自然要有Omit(一个是从对象中挑选部分,一个是排除部分),它和Pick的写法非常像,但有一个问题要解决:我们要怎么表示T中剔除了K后的剩余字段?

Pick 选取传入的键值,Omit 移除传入的键值

这里我们又要引入一个知识点:never类型,它表示永远不会出现的类型,通常被用来将收窄联合类型或是接口,或者作为条件类型判断的兜底。

Exclude,字面意思看起来是排除,那么第一个参数应该是要进行筛选的,第二个应该是筛选条件!

接地气的版本:"1"在 “1” | “2” 里面吗( “1” extends “1”|“2” -> true )? 在的话,就剔除掉它(赋值为never),不在的话就保留。

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

那么 Omit就很简单了,对原接口的成员,剔除掉传入的联合类型成员,应用 Pick 即可。

type Omit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>;
几乎所有使用条件类型的场景,把判断后的赋值语句反一下,就会有新的场景
type Extract<T, U> = T extends U ? T : never;

再来看个常用的工具类型 Record<Keys, Type>,通常用于生成以联合类型为键名(Keys),键值类型为Type的新接口,比如:

type MyNav = "a" | "b" | "b";
interface INavWidgets {
  widgets: string[];
  title?: string;
  keepAlive?: boolean;
}
const router: Record<MyNav, INavWidgets> = {
  a: { widget: [""] },
  b: { widget: [""] },
  c: { widget: [""] },
};

其实很简单,把 Keys 的每个键值拿出来,类型规定为 Type 即可

// K extends keyof any 约束K必须为联合类型
type Record<K extends keyof any, T> = {
  [P in K]: T;
};

前面的ReturnType把 infer 换个位置,比如放到入参处,它就变成了获取参数类型的Parameters:

type Parameters<T extends (...args: any) => any> = T extends (
  ...args: infer P
) => any
  ? P
  : never;

把普通函数换成类的构造函数,就得到了类构造函数入参类型的ConstructorParameters

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

加上new关键字来使其成为可实例化类型声明,即此处约束泛型为类。

如果把待 infer 的类型放到其返回处,想想 new 一个类的返回值是什么?实例!所以我们得到了实例类型InstanceType

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

你应该已经 get 到了那么一丝天机

社区工具类型

这一部分的工具类型大多来自于utility-types,其作者同时还有 react-redux-typescript-guide 和 typesafe-actions 这两个优秀作品。
同时,也推荐 type-fest 这个库,和上面相比更加接地气一些。其作者的作品…,我保证你直接或间接的使用过(如果不信,一定要去看看,我刚看到的时候是真的震惊的不行)。

我们由浅入深,先封装基础的类型别名和对应的类型守卫:

export type Primitive =
  | string
  | number
  | bigint
  | boolean
  | symbol
  | null
  | undefined;

export const isPrimitive = (val: unknown): val is Primitive => {
  if (val === null || val === undefined) {
    return true;
  }

  const typeDef = typeof val;

  const primitiveNonNullishTypes = [
    "string",
    "number",
    "bigint",
    "boolean",
    "symbol",
  ];

  return primitiveNonNullishTypes.indexOf(typeDef) !== -1;
};

export type Nullish = null | undefined;

export type NonUndefined<A> = A extends undefined ? never : A;
// 实际上TS也内置了这个工具类型
type NonNullable<T> = T extends null | undefined ? never : T;

常用的场景提取 Promise 的实际类型

const foo = (): Promise<string> => {
  return new Promise((resolve, reject) => {
    resolve("linbudu");
  });
};

// Promise<string>
type FooReturnType = ReturnType<typeof foo>;

// string
type NakedFooReturnType = PromiseType<FooReturnType>;

如果你已经熟练掌握了infer的使用,那么实际上是很好写的,只需要用一个infer参数作为 Promise 的泛型即可:

export type PromiseType<T extends Promise<any>> = T extends Promise<infer U>
  ? U
  : never;
递归工具类

如果接口中存在着嵌套呢

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

如果是对象类型,那就遍历这个对象内部

export type DeepPartial<T> = {
  [P in keyof T]?: T[P] extends object ? DeepPartial<T[P]> : T[P];
};

utility-types内部的实现实际比这个复杂,还考虑了数组的情况,这里为了便于理解做了简化

学习完产生的疑问

in和extends关键字的语义好像有很多 感觉挺迷的

in 关键字
type StringifyA<T> = { [K in keyof T]: string; }; //在这里可以理解成for...in
function useIt(arg: A | B): void { 'a' in arg ? arg.useA() : arg.useB(); } //它能够判断一个属性是否为对象所拥有:
extends 关键字
type Exclude<T, U> = T extends U ? never : T; //这里为true的情况是U包含于T
T extends U ? X : Y //如果你觉得这里的 extends 不太好理解,可以暂时简单理解为 U 中的属性在 T 中都有。
按照这个说法的话 又是T包含于U

疑问的解答

学习到 类型协变的时候 理解了extends 子类型比父类型要更具体
第一个是联合类型 子类型比父类型更具体包含于T
第二个是对象 子类型具有父类所有特点又可以添加新的行文 也更加具体

同理 in关键字
要区分是联合类型还是对象 联合类型就是遍历
对象的话就是 判断一个属性是否为对象所拥有


遇到难以理解的一定要手敲一边 创建 使用

in 和 extends 疑问

什么是类型编程

从一个简简单单的interface,到看起来挺高级的T extends SomeType ,再到各种不明觉厉的工具类型Partial、Required等,这些都属于类型编程的范畴。

正文

1.类型编程的基础:泛型

之所以上来就放泛型,是因为在 TypeScript 的整个类型编程体系中,它是最基础的那部分,所有的进阶类型都基于它书写。就像编程时我们不能没有变量,类型编程中的变量就是泛型

1.1基本写法
1.1.1Function中的写法参数和返回值相同

例子

function foo<T>(arg: T): T {
  return arg;
}

优点与规范

1.1.1泛型使得代码段的类型定义易于重用提升了灵活性与严谨性
1.1.2通常泛型。如 T U K V S等。项目达到一定复杂度后,使用带有具体意义的泛型变量声明,如 BasicBusinessType 这种形式。

1.1.2箭头函数中的写法
//第一种
const foo = <T>(arg:T) => arg
foo<string>("1")

//第二种
const templateTypeArr: <T>(pams: T) => T =params=> {
        return params;
    }
templateTypeArr<string>("1")
    
//在 TSX 文件中这么写,<T>可能会被识别为 JSX 标签,因此需要显式告知编译器
const foo = <T extends SomeBasicType>(arg: T) => arg;
1.1.3类中的写法
class Foo<T, U> {
  constructor(public arg1: T, public arg2: U) {}

  public method(): T {
    return this.arg1;
  }
}
new Foo<string,number>("1",1)
类型守卫、is in关键字
is关键字使用
/**
 * undefind、null、NaN、空数组、空字符串
 * @param {*} value - 数据
 * @returns {boolean}
 */
export function isEmpty(value: any): value is undefined | null | typeof NaN | [] | '';
if (isEmpty([])) console.log("是空的")
in关键字使用
class A {
  public a() {}

  public useA() {
    return "A";
  }
}

class B {
  public b() {}

  public useB() {
    return "B";
  }
}
//它能够判断一个属性是否为对象所拥有:
function useIt(arg: A | B): void {
  'a' in arg ? arg.useA() : arg.useB();
}
索引类型与映射类型
1. 索引类型
例子
1.1基本写法
// 假设key是obj键名
function pickSingleValue(obj, key) {
  return obj[key];
}

function pickSingleValue<T>(obj: T, key: keyof T): T[keyof T] {
  return obj[key];
}

keyof 是 索引类型查询 的语法, 它会返回后面跟着的类型参数的键值组成的字面量联合类型,

interface foo {
  a: number;
  b: string;
}
type A = keyof foo; // "a" | "b"
type PropAType = T["a"]; // number
1.2进阶写法

可以改进的地方
keyof出现了两次,以及泛型 T 其实应该被限制为对象类型。对于第一点,就像我们平时编程会做的那样:用一个变量把多处出现的存起来,记得,在类型编程里,泛型就是变量。

function pickSingleValue<T extends object, U extends keyof T>(
  obj: T,
  key: U
): T[U] {
  return obj[key];
}

extends
T extends object 理解为T 被限制为对象类型
U extends keyof T理解为 泛型 U 必然是泛型 T 的键名组成的联合类型(以字面量类型的形式,比如T这个对象的键名包括a b c,那么U的取值只能是"a" “b” "c"之一,即 “a” | “b” | “c”)
具体细节我们会在 条件类型 一章讲到。

上个例子的扩展

我们要取出一系列值,即参数 2 将是一个数组,成员均为参数 1 的键名组成:

function pick<T extends object, U extends keyof T>(obj: T, keys: U[]): T[U][] {
  return keys.map((key) => obj[key]);
}

// pick(obj, ['a', 'b'])

keys: U[] 我们知道 U 是 T 的键名组成的联合类型,那么要表示一个内部元素均是 T 键名的数组,就可以使用这种方式,具体的原理请参见下文的 分布式条件类型 章节

T[U][] 它的原理实际上和上面一条相同,首先是T[U],代表参数1的键值(就像Object[Key]),我认为它是一个很好地例子,表现了 TS 类型编程的组合性,你不感觉这种写法就像搭积木一样吗?

索引签名 Index Signature

在JavaScript中,我们通常使用 arr[1] 的方式索引数组,使用 obj[key] 的方式索引对象。说白了,索引就是你获取一个对象成员的方式,而在类型编程中,索引签名用于快速建立一个内部字段类型相同的接口,如

interface Foo {
  [keys: string]: string;
}

值得注意的是,由于 JS 可以同时通过数字与字符串访问对象属性,因此keyof Foo的结果会是string | numbe

//js代码
const o: Foo = {
 1: "芜湖!",
};

o[1] === o["1"]; // true

但是一旦某个接口的索引签名类型为number,那么使用它的对象就不能再通过字符串索引访问,如o[‘1’],将会抛出错误, 元素隐式具有 “any” 类型,因为索引表达式的类型不为 “number”。

映射类型 Mapped Types

在开始映射类型前,首先想想 JavaScript 中数组的 map 方法,通过使用map,我们从一个数组按照既定的映射关系获得一个新的数组。在类型编程中,我们则会从一个类型定义(包括但不限于接口、类型别名)映射得到一个新的类型定义。通常会在旧有类型的基础上进行改造,如:

例子

interface A {
  a: boolean;
  b: string;
  c: number;
  d: () => void;
}

现在我们有个需求,实现一个接口,它的字段与接口 A 完全相同,但是其中的类型全部为 string

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

重要的就是这个in操作符,你完全可以把它理解为 for…in/for…of 这种遍历的思路,获取到键名之后,键值就简单了,所以我们可以很容易的拷贝一个新的类型别名出来。

不是变量(泛型)是个特定属性名就会判断存不存在
是变量(泛型)可以把它理解为 for…in/

条件类型 Conditional Types

条件类型 的语法,实际上就是三元表达式

T extends U ? X : Y
//如果你觉得这里的 extends 不太好理解,可以暂时简单理解为 U 中的属性在 T 中都有。

类比到编程语句中,其实就是根据条件判断来动态的赋予变量值:

let unknownVar: string;

unknownVar = condition ? "淘系前端" : "淘宝FED";

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

又是一个例子使用条件类型作为函数返回值类型的例子

declare function strOrNum<T extends boolean>(
  x: T
): T extends true ? string : number;

在这种情况下,条件类型的推导就会被延迟,因为此时类型系统没有足够的信息来完成判断。只有给出了所需信息(在这里是入参 x 的类型),才可以完成推导。

const strReturnType = strOrNum(true);
const numReturnType = strOrNum(false);

就像三元表达式可以嵌套,条件类型也可以嵌套

type TypeName<T> = T extends string
  ? "string"
  : T extends number
  ? "number"
  : T extends boolean
  ? "boolean"
  : T extends undefined
  ? "undefined"
  : T extends Function
  ? "function"
  : "object";
分布式条件类型 Distributive Conditional Types

分布式条件类型实际上不是一种特殊的条件类型,而是其特性之一(所以说条件类型的分布式特性更为准确)。我们直接先上概念: 对于属于裸类型参数的检查类型,条件类型会在实例化时期自动分发到联合类型上

原文:
Conditional types in which the checked type is a naked type parameter are called distributive conditional types. Distributive conditional types are automatically distributed over union types during instantiation

  • 裸类型参数(就是泛型就是类型参数)
  • 实例化
  • 分发到联合类型
使用上面的TypeName类型别名
// 使用上面的TypeName类型别名
// "string" | "function"
type T1 = TypeName<string | (() => void)>;
// "string" | "object"
type T2 = TypeName<string | string[]>;
// "object"
type T3 = TypeName<string[] | number[]>;

上面的例子里,条件类型的推导结果都是联合类型(T3 实际上也是,只不过因为结果相同所以被合并了)
其实就是类型参数被依次进行条件判断后,再使用|组合得来的结果。

上面的例子中泛型都是裸露着的,如果被包裹着,其条件类型判断结果会有什么变化吗

还是例子
type Naked<T> = T extends boolean ? "Y" : "N";
type Wrapped<T> = [T] extends [boolean] ? "Y" : "N";

// "N" | "Y"
type Distributed = Naked<number | boolean>;

// "N"
type NotDistributed = Wrapped<number | boolean>;
  • 其中,Distributed类型别名,其类型参数(number | boolean)会正确的分发,即
    先分发到 Naked | Naked,再进行判断,所以结果是"N" | “Y”。
  • 而 NotDistributed 类型别名,第一眼看上去感觉TS应该会自动按数组进行分发,结果应该也是 “N” | “Y” ?但实际上,它的类型参数(number | boolean)不会有分发流程,直接进行[number | boolean] extends [boolean]的判断,所以结果是"N"。
几个概念
  1. 裸类型参数,没有额外被[]包裹过的,就像被数组包裹后就不能再被称为裸类型参数。
  2. 实例化,其实就是条件类型的判断过程,就像我们前面说的,条件类型需要在收集到足够的推断信息之后才能进行这个过程。在这里两个例子的实例化过程实际上是不同的,具体会在下一点中介绍。
  3. 分发到联合类型:
  4. 对于 TypeName,它内部的类型参数 T 是没有被包裹过的,所以 TypeName<string | (() => void)> 会被分发为 TypeName | TypeName<(() => void)>,然后再次进行判断,最后分发为"string" | “function”。
( A | B | C ) extends T ? X : Y
// 相当于
(A extends T ? X : Y) | (B extends T ? X : Y) | (B extends T ? X : Y)

// 使用[]包裹后,不会进行额外的分发逻辑。
[A | B | C] extends [T] ? X : Y

没有被 [] 额外包装的联合类型参数,在条件类型进行判定时会将联合类型分发,分别进行判断。

这两种行为没有好坏之分,区别只在于是否进行联合类型的分发,如果你需要走分布式条件类型,那么注意保持你的类型参数为裸类型参数。如果你想避免这种行为,那么使用 [] 包裹你的类型参数即可(注意在 extends 关键字的两侧都需要)。

infer关键字
在条件类型中,我们展示了如何通过条件判断来延迟确定类型,但仅仅使用条件类型也有一定不足:
它无法从条件上得到类型信息。举例来说,T extends Array<PrimitiveType> ? "foo" : "bar"
这一例子,我们不能从作为条件的 Array<PrimitiveType> 中获取到 PrimitiveType 的实际类型。

而这样的场景又是十分常见的,如获取函数返回值的类型、拆箱Promise / 数组等,因此这一节我们来介绍下 infer 关键字。

看一个简单的例子,用于获取函数返回值类型的工具类型ReturnType

const foo = (): string => {
  return "linbudu";
};

type ReturnType<T> = T extends (...args: any[]) => infer R ? R : never;

// string
type FooReturnType = ReturnType<typeof foo>;
  1. (...args: any[]) => infer R 是一个整体,这里函数的返回值类型的位置被 infer R 占据了。
  2. 当 ReturnType 被调用,类型参数 T 、R 被显式赋值(T为 typeof foo,infer R被整体赋值为string,即函数的返回值类型),如果 T 满足条件类型的约束,就返回nfer 完毕的R 的值,在这里 R 即为函数的返回值实际类型。
  3. 严谨写法
// 第一个 extends 约束可传入的泛型只能为函数类型
// 第二个 extends 作为条件判断
type ReturnType<T extends (...args: any[]) => any> = T extends (...args: any[]) => infer R ? R : never;
工具类型 Tool Type
type Partial<T> = {
  [K in keyof T]?: T[k];
};

它用于将一个接口中的字段全部变为可选,除了索引类型以及映射类型以外,它只使用了?可选修饰符,那么我现在直接掏出小抄:

  1. 去除可选修饰符:-?,位置与 ? 一致
  2. 只读修饰符:readonly,位置在键名,如 readonly key: string
  3. 去除只读修饰符:-readonly,位置同readonly
type Required<T> = {
  [K in keyof T]-?: T[K];
};

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

在上面我们实现了一个pick函数

function pick<T extends object, U extends keyof T>(obj: T, keys: U[]): T[U][] {
  return keys.map((key) => obj[key]);
}

类似的,假设我们现在需要从一个接口中挑选一些字段:

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

// 期望用法
// 期望结果 A["a"]类型 | A["b"]类型
type Part = Pick<A, "a" | "b">;

还是映射类型,只不过现在映射类型的映射源是传入给 Pick 的类型参数K。

既然有了Pick,那么自然要有Omit(一个是从对象中挑选部分,一个是排除部分),它和Pick的写法非常像,但有一个问题要解决:我们要怎么表示T中剔除了K后的剩余字段?

Pick 选取传入的键值,Omit 移除传入的键值

这里我们又要引入一个知识点:never类型,它表示永远不会出现的类型,通常被用来将收窄联合类型或是接口,或者作为条件类型判断的兜底。

Exclude,字面意思看起来是排除,那么第一个参数应该是要进行筛选的,第二个应该是筛选条件!

接地气的版本:"1"在 “1” | “2” 里面吗( “1” extends “1”|“2” -> true )? 在的话,就剔除掉它(赋值为never),不在的话就保留。

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

那么 Omit就很简单了,对原接口的成员,剔除掉传入的联合类型成员,应用 Pick 即可。

type Omit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>;
几乎所有使用条件类型的场景,把判断后的赋值语句反一下,就会有新的场景
type Extract<T, U> = T extends U ? T : never;

再来看个常用的工具类型 Record<Keys, Type>,通常用于生成以联合类型为键名(Keys),键值类型为Type的新接口,比如:

type MyNav = "a" | "b" | "b";
interface INavWidgets {
  widgets: string[];
  title?: string;
  keepAlive?: boolean;
}
const router: Record<MyNav, INavWidgets> = {
  a: { widget: [""] },
  b: { widget: [""] },
  c: { widget: [""] },
};

其实很简单,把 Keys 的每个键值拿出来,类型规定为 Type 即可

// K extends keyof any 约束K必须为联合类型
type Record<K extends keyof any, T> = {
  [P in K]: T;
};

前面的ReturnType把 infer 换个位置,比如放到入参处,它就变成了获取参数类型的Parameters:

type Parameters<T extends (...args: any) => any> = T extends (
  ...args: infer P
) => any
  ? P
  : never;

把普通函数换成类的构造函数,就得到了类构造函数入参类型的ConstructorParameters

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

加上new关键字来使其成为可实例化类型声明,即此处约束泛型为类。

如果把待 infer 的类型放到其返回处,想想 new 一个类的返回值是什么?实例!所以我们得到了实例类型InstanceType

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

你应该已经 get 到了那么一丝天机

社区工具类型

这一部分的工具类型大多来自于utility-types,其作者同时还有 react-redux-typescript-guide 和 typesafe-actions 这两个优秀作品。
同时,也推荐 type-fest 这个库,和上面相比更加接地气一些。其作者的作品…,我保证你直接或间接的使用过(如果不信,一定要去看看,我刚看到的时候是真的震惊的不行)。

我们由浅入深,先封装基础的类型别名和对应的类型守卫:

export type Primitive =
  | string
  | number
  | bigint
  | boolean
  | symbol
  | null
  | undefined;

export const isPrimitive = (val: unknown): val is Primitive => {
  if (val === null || val === undefined) {
    return true;
  }

  const typeDef = typeof val;

  const primitiveNonNullishTypes = [
    "string",
    "number",
    "bigint",
    "boolean",
    "symbol",
  ];

  return primitiveNonNullishTypes.indexOf(typeDef) !== -1;
};

export type Nullish = null | undefined;

export type NonUndefined<A> = A extends undefined ? never : A;
// 实际上TS也内置了这个工具类型
type NonNullable<T> = T extends null | undefined ? never : T;

常用的场景提取 Promise 的实际类型

const foo = (): Promise<string> => {
  return new Promise((resolve, reject) => {
    resolve("linbudu");
  });
};

// Promise<string>
type FooReturnType = ReturnType<typeof foo>;

// string
type NakedFooReturnType = PromiseType<FooReturnType>;

如果你已经熟练掌握了infer的使用,那么实际上是很好写的,只需要用一个infer参数作为 Promise 的泛型即可:

export type PromiseType<T extends Promise<any>> = T extends Promise<infer U>
  ? U
  : never;
递归工具类

如果接口中存在着嵌套呢

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

如果是对象类型,那就遍历这个对象内部

export type DeepPartial<T> = {
  [P in keyof T]?: T[P] extends object ? DeepPartial<T[P]> : T[P];
};

utility-types内部的实现实际比这个复杂,还考虑了数组的情况,这里为了便于理解做了简化

学习完产生的疑问

in和extends关键字的语义好像有很多 感觉挺迷的

in 关键字
type StringifyA<T> = { [K in keyof T]: string; }; //在这里可以理解成for...in
function useIt(arg: A | B): void { 'a' in arg ? arg.useA() : arg.useB(); } //它能够判断一个属性是否为对象所拥有:
extends 关键字
type Exclude<T, U> = T extends U ? never : T; //这里为true的情况是U包含于T
T extends U ? X : Y //如果你觉得这里的 extends 不太好理解,可以暂时简单理解为 U 中的属性在 T 中都有。
按照这个说法的话 又是T包含于U

疑问的解答

学习到 类型协变的时候 理解了extends 子类型比父类型要更具体
第一个是联合类型 子类型比父类型更具体包含于T
第二个是对象 子类型具有父类所有特点又可以添加新的行文 也更加具体

同理 in关键字
要区分是联合类型还是对象 联合类型就是遍历
对象的话就是 判断一个属性是否为对象所拥有


遇到难以理解的一定要手敲一边 创建 使用

in 和 extends 疑问

什么是类型编程

从一个简简单单的interface,到看起来挺高级的T extends SomeType ,再到各种不明觉厉的工具类型Partial、Required等,这些都属于类型编程的范畴。

正文

1.类型编程的基础:泛型

之所以上来就放泛型,是因为在 TypeScript 的整个类型编程体系中,它是最基础的那部分,所有的进阶类型都基于它书写。就像编程时我们不能没有变量,类型编程中的变量就是泛型

1.1基本写法
1.1.1Function中的写法参数和返回值相同

例子

function foo<T>(arg: T): T {
  return arg;
}

优点与规范

1.1.1泛型使得代码段的类型定义易于重用提升了灵活性与严谨性
1.1.2通常泛型。如 T U K V S等。项目达到一定复杂度后,使用带有具体意义的泛型变量声明,如 BasicBusinessType 这种形式。

1.1.2箭头函数中的写法
//第一种
const foo = <T>(arg:T) => arg
foo<string>("1")

//第二种
const templateTypeArr: <T>(pams: T) => T =params=> {
        return params;
    }
templateTypeArr<string>("1")
    
//在 TSX 文件中这么写,<T>可能会被识别为 JSX 标签,因此需要显式告知编译器
const foo = <T extends SomeBasicType>(arg: T) => arg;
1.1.3类中的写法
class Foo<T, U> {
  constructor(public arg1: T, public arg2: U) {}

  public method(): T {
    return this.arg1;
  }
}
new Foo<string,number>("1",1)
类型守卫、is in关键字
is关键字使用
/**
 * undefind、null、NaN、空数组、空字符串
 * @param {*} value - 数据
 * @returns {boolean}
 */
export function isEmpty(value: any): value is undefined | null | typeof NaN | [] | '';
if (isEmpty([])) console.log("是空的")
in关键字使用
class A {
  public a() {}

  public useA() {
    return "A";
  }
}

class B {
  public b() {}

  public useB() {
    return "B";
  }
}
//它能够判断一个属性是否为对象所拥有:
function useIt(arg: A | B): void {
  'a' in arg ? arg.useA() : arg.useB();
}
索引类型与映射类型
1. 索引类型
例子
1.1基本写法
// 假设key是obj键名
function pickSingleValue(obj, key) {
  return obj[key];
}

function pickSingleValue<T>(obj: T, key: keyof T): T[keyof T] {
  return obj[key];
}

keyof 是 索引类型查询 的语法, 它会返回后面跟着的类型参数的键值组成的字面量联合类型,

interface foo {
  a: number;
  b: string;
}
type A = keyof foo; // "a" | "b"
type PropAType = T["a"]; // number
1.2进阶写法

可以改进的地方
keyof出现了两次,以及泛型 T 其实应该被限制为对象类型。对于第一点,就像我们平时编程会做的那样:用一个变量把多处出现的存起来,记得,在类型编程里,泛型就是变量。

function pickSingleValue<T extends object, U extends keyof T>(
  obj: T,
  key: U
): T[U] {
  return obj[key];
}

extends
T extends object 理解为T 被限制为对象类型
U extends keyof T理解为 泛型 U 必然是泛型 T 的键名组成的联合类型(以字面量类型的形式,比如T这个对象的键名包括a b c,那么U的取值只能是"a" “b” "c"之一,即 “a” | “b” | “c”)
具体细节我们会在 条件类型 一章讲到。

上个例子的扩展

我们要取出一系列值,即参数 2 将是一个数组,成员均为参数 1 的键名组成:

function pick<T extends object, U extends keyof T>(obj: T, keys: U[]): T[U][] {
  return keys.map((key) => obj[key]);
}

// pick(obj, ['a', 'b'])

keys: U[] 我们知道 U 是 T 的键名组成的联合类型,那么要表示一个内部元素均是 T 键名的数组,就可以使用这种方式,具体的原理请参见下文的 分布式条件类型 章节

T[U][] 它的原理实际上和上面一条相同,首先是T[U],代表参数1的键值(就像Object[Key]),我认为它是一个很好地例子,表现了 TS 类型编程的组合性,你不感觉这种写法就像搭积木一样吗?

索引签名 Index Signature

在JavaScript中,我们通常使用 arr[1] 的方式索引数组,使用 obj[key] 的方式索引对象。说白了,索引就是你获取一个对象成员的方式,而在类型编程中,索引签名用于快速建立一个内部字段类型相同的接口,如

interface Foo {
  [keys: string]: string;
}

值得注意的是,由于 JS 可以同时通过数字与字符串访问对象属性,因此keyof Foo的结果会是string | numbe

//js代码
const o: Foo = {
 1: "芜湖!",
};

o[1] === o["1"]; // true

但是一旦某个接口的索引签名类型为number,那么使用它的对象就不能再通过字符串索引访问,如o[‘1’],将会抛出错误, 元素隐式具有 “any” 类型,因为索引表达式的类型不为 “number”。

映射类型 Mapped Types

在开始映射类型前,首先想想 JavaScript 中数组的 map 方法,通过使用map,我们从一个数组按照既定的映射关系获得一个新的数组。在类型编程中,我们则会从一个类型定义(包括但不限于接口、类型别名)映射得到一个新的类型定义。通常会在旧有类型的基础上进行改造,如:

例子

interface A {
  a: boolean;
  b: string;
  c: number;
  d: () => void;
}

现在我们有个需求,实现一个接口,它的字段与接口 A 完全相同,但是其中的类型全部为 string

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

重要的就是这个in操作符,你完全可以把它理解为 for…in/for…of 这种遍历的思路,获取到键名之后,键值就简单了,所以我们可以很容易的拷贝一个新的类型别名出来。

不是变量(泛型)是个特定属性名就会判断存不存在
是变量(泛型)可以把它理解为 for…in/

条件类型 Conditional Types

条件类型 的语法,实际上就是三元表达式

T extends U ? X : Y
//如果你觉得这里的 extends 不太好理解,可以暂时简单理解为 U 中的属性在 T 中都有。

类比到编程语句中,其实就是根据条件判断来动态的赋予变量值:

let unknownVar: string;

unknownVar = condition ? "淘系前端" : "淘宝FED";

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

又是一个例子使用条件类型作为函数返回值类型的例子

declare function strOrNum<T extends boolean>(
  x: T
): T extends true ? string : number;

在这种情况下,条件类型的推导就会被延迟,因为此时类型系统没有足够的信息来完成判断。只有给出了所需信息(在这里是入参 x 的类型),才可以完成推导。

const strReturnType = strOrNum(true);
const numReturnType = strOrNum(false);

就像三元表达式可以嵌套,条件类型也可以嵌套

type TypeName<T> = T extends string
  ? "string"
  : T extends number
  ? "number"
  : T extends boolean
  ? "boolean"
  : T extends undefined
  ? "undefined"
  : T extends Function
  ? "function"
  : "object";
分布式条件类型 Distributive Conditional Types

分布式条件类型实际上不是一种特殊的条件类型,而是其特性之一(所以说条件类型的分布式特性更为准确)。我们直接先上概念: 对于属于裸类型参数的检查类型,条件类型会在实例化时期自动分发到联合类型上

原文:
Conditional types in which the checked type is a naked type parameter are called distributive conditional types. Distributive conditional types are automatically distributed over union types during instantiation

  • 裸类型参数(就是泛型就是类型参数)
  • 实例化
  • 分发到联合类型
使用上面的TypeName类型别名
// 使用上面的TypeName类型别名
// "string" | "function"
type T1 = TypeName<string | (() => void)>;
// "string" | "object"
type T2 = TypeName<string | string[]>;
// "object"
type T3 = TypeName<string[] | number[]>;

上面的例子里,条件类型的推导结果都是联合类型(T3 实际上也是,只不过因为结果相同所以被合并了)
其实就是类型参数被依次进行条件判断后,再使用|组合得来的结果。

上面的例子中泛型都是裸露着的,如果被包裹着,其条件类型判断结果会有什么变化吗

还是例子
type Naked<T> = T extends boolean ? "Y" : "N";
type Wrapped<T> = [T] extends [boolean] ? "Y" : "N";

// "N" | "Y"
type Distributed = Naked<number | boolean>;

// "N"
type NotDistributed = Wrapped<number | boolean>;
  • 其中,Distributed类型别名,其类型参数(number | boolean)会正确的分发,即
    先分发到 Naked | Naked,再进行判断,所以结果是"N" | “Y”。
  • 而 NotDistributed 类型别名,第一眼看上去感觉TS应该会自动按数组进行分发,结果应该也是 “N” | “Y” ?但实际上,它的类型参数(number | boolean)不会有分发流程,直接进行[number | boolean] extends [boolean]的判断,所以结果是"N"。
几个概念
  1. 裸类型参数,没有额外被[]包裹过的,就像被数组包裹后就不能再被称为裸类型参数。
  2. 实例化,其实就是条件类型的判断过程,就像我们前面说的,条件类型需要在收集到足够的推断信息之后才能进行这个过程。在这里两个例子的实例化过程实际上是不同的,具体会在下一点中介绍。
  3. 分发到联合类型:
  4. 对于 TypeName,它内部的类型参数 T 是没有被包裹过的,所以 TypeName<string | (() => void)> 会被分发为 TypeName | TypeName<(() => void)>,然后再次进行判断,最后分发为"string" | “function”。
( A | B | C ) extends T ? X : Y
// 相当于
(A extends T ? X : Y) | (B extends T ? X : Y) | (B extends T ? X : Y)

// 使用[]包裹后,不会进行额外的分发逻辑。
[A | B | C] extends [T] ? X : Y

没有被 [] 额外包装的联合类型参数,在条件类型进行判定时会将联合类型分发,分别进行判断。

这两种行为没有好坏之分,区别只在于是否进行联合类型的分发,如果你需要走分布式条件类型,那么注意保持你的类型参数为裸类型参数。如果你想避免这种行为,那么使用 [] 包裹你的类型参数即可(注意在 extends 关键字的两侧都需要)。

infer关键字
在条件类型中,我们展示了如何通过条件判断来延迟确定类型,但仅仅使用条件类型也有一定不足:
它无法从条件上得到类型信息。举例来说,T extends Array<PrimitiveType> ? "foo" : "bar"
这一例子,我们不能从作为条件的 Array<PrimitiveType> 中获取到 PrimitiveType 的实际类型。

而这样的场景又是十分常见的,如获取函数返回值的类型、拆箱Promise / 数组等,因此这一节我们来介绍下 infer 关键字。

看一个简单的例子,用于获取函数返回值类型的工具类型ReturnType

const foo = (): string => {
  return "linbudu";
};

type ReturnType<T> = T extends (...args: any[]) => infer R ? R : never;

// string
type FooReturnType = ReturnType<typeof foo>;
  1. (...args: any[]) => infer R 是一个整体,这里函数的返回值类型的位置被 infer R 占据了。
  2. 当 ReturnType 被调用,类型参数 T 、R 被显式赋值(T为 typeof foo,infer R被整体赋值为string,即函数的返回值类型),如果 T 满足条件类型的约束,就返回nfer 完毕的R 的值,在这里 R 即为函数的返回值实际类型。
  3. 严谨写法
// 第一个 extends 约束可传入的泛型只能为函数类型
// 第二个 extends 作为条件判断
type ReturnType<T extends (...args: any[]) => any> = T extends (...args: any[]) => infer R ? R : never;
工具类型 Tool Type
type Partial<T> = {
  [K in keyof T]?: T[k];
};

它用于将一个接口中的字段全部变为可选,除了索引类型以及映射类型以外,它只使用了?可选修饰符,那么我现在直接掏出小抄:

  1. 去除可选修饰符:-?,位置与 ? 一致
  2. 只读修饰符:readonly,位置在键名,如 readonly key: string
  3. 去除只读修饰符:-readonly,位置同readonly
type Required<T> = {
  [K in keyof T]-?: T[K];
};

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

在上面我们实现了一个pick函数

function pick<T extends object, U extends keyof T>(obj: T, keys: U[]): T[U][] {
  return keys.map((key) => obj[key]);
}

类似的,假设我们现在需要从一个接口中挑选一些字段:

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

// 期望用法
// 期望结果 A["a"]类型 | A["b"]类型
type Part = Pick<A, "a" | "b">;

还是映射类型,只不过现在映射类型的映射源是传入给 Pick 的类型参数K。

既然有了Pick,那么自然要有Omit(一个是从对象中挑选部分,一个是排除部分),它和Pick的写法非常像,但有一个问题要解决:我们要怎么表示T中剔除了K后的剩余字段?

Pick 选取传入的键值,Omit 移除传入的键值

这里我们又要引入一个知识点:never类型,它表示永远不会出现的类型,通常被用来将收窄联合类型或是接口,或者作为条件类型判断的兜底。

Exclude,字面意思看起来是排除,那么第一个参数应该是要进行筛选的,第二个应该是筛选条件!

接地气的版本:"1"在 “1” | “2” 里面吗( “1” extends “1”|“2” -> true )? 在的话,就剔除掉它(赋值为never),不在的话就保留。

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

那么 Omit就很简单了,对原接口的成员,剔除掉传入的联合类型成员,应用 Pick 即可。

type Omit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>;
几乎所有使用条件类型的场景,把判断后的赋值语句反一下,就会有新的场景
type Extract<T, U> = T extends U ? T : never;

再来看个常用的工具类型 Record<Keys, Type>,通常用于生成以联合类型为键名(Keys),键值类型为Type的新接口,比如:

type MyNav = "a" | "b" | "b";
interface INavWidgets {
  widgets: string[];
  title?: string;
  keepAlive?: boolean;
}
const router: Record<MyNav, INavWidgets> = {
  a: { widget: [""] },
  b: { widget: [""] },
  c: { widget: [""] },
};

其实很简单,把 Keys 的每个键值拿出来,类型规定为 Type 即可

// K extends keyof any 约束K必须为联合类型
type Record<K extends keyof any, T> = {
  [P in K]: T;
};

前面的ReturnType把 infer 换个位置,比如放到入参处,它就变成了获取参数类型的Parameters:

type Parameters<T extends (...args: any) => any> = T extends (
  ...args: infer P
) => any
  ? P
  : never;

把普通函数换成类的构造函数,就得到了类构造函数入参类型的ConstructorParameters

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

加上new关键字来使其成为可实例化类型声明,即此处约束泛型为类。

如果把待 infer 的类型放到其返回处,想想 new 一个类的返回值是什么?实例!所以我们得到了实例类型InstanceType

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

你应该已经 get 到了那么一丝天机

社区工具类型

这一部分的工具类型大多来自于utility-types,其作者同时还有 react-redux-typescript-guide 和 typesafe-actions 这两个优秀作品。
同时,也推荐 type-fest 这个库,和上面相比更加接地气一些。其作者的作品…,我保证你直接或间接的使用过(如果不信,一定要去看看,我刚看到的时候是真的震惊的不行)。

我们由浅入深,先封装基础的类型别名和对应的类型守卫:

export type Primitive =
  | string
  | number
  | bigint
  | boolean
  | symbol
  | null
  | undefined;

export const isPrimitive = (val: unknown): val is Primitive => {
  if (val === null || val === undefined) {
    return true;
  }

  const typeDef = typeof val;

  const primitiveNonNullishTypes = [
    "string",
    "number",
    "bigint",
    "boolean",
    "symbol",
  ];

  return primitiveNonNullishTypes.indexOf(typeDef) !== -1;
};

export type Nullish = null | undefined;

export type NonUndefined<A> = A extends undefined ? never : A;
// 实际上TS也内置了这个工具类型
type NonNullable<T> = T extends null | undefined ? never : T;

常用的场景提取 Promise 的实际类型

const foo = (): Promise<string> => {
  return new Promise((resolve, reject) => {
    resolve("linbudu");
  });
};

// Promise<string>
type FooReturnType = ReturnType<typeof foo>;

// string
type NakedFooReturnType = PromiseType<FooReturnType>;

如果你已经熟练掌握了infer的使用,那么实际上是很好写的,只需要用一个infer参数作为 Promise 的泛型即可:

export type PromiseType<T extends Promise<any>> = T extends Promise<infer U>
  ? U
  : never;
递归工具类

如果接口中存在着嵌套呢

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

如果是对象类型,那就遍历这个对象内部

export type DeepPartial<T> = {
  [P in keyof T]?: T[P] extends object ? DeepPartial<T[P]> : T[P];
};

utility-types内部的实现实际比这个复杂,还考虑了数组的情况,这里为了便于理解做了简化

学习完产生的疑问

in和extends关键字的语义好像有很多 感觉挺迷的

in 关键字
type StringifyA<T> = { [K in keyof T]: string; }; //在这里可以理解成for...in
function useIt(arg: A | B): void { 'a' in arg ? arg.useA() : arg.useB(); } //它能够判断一个属性是否为对象所拥有:
extends 关键字
type Exclude<T, U> = T extends U ? never : T; //这里为true的情况是U包含于T
T extends U ? X : Y //如果你觉得这里的 extends 不太好理解,可以暂时简单理解为 U 中的属性在 T 中都有。
按照这个说法的话 又是T包含于U

疑问的解答

学习到 类型协变的时候 理解了extends 子类型比父类型要更具体
第一个是联合类型 子类型比父类型更具体包含于T
第二个是对象 子类型具有父类所有特点又可以添加新的行文 也更加具体

同理 in关键字
要区分是联合类型还是对象 联合类型就是遍历
对象的话就是 判断一个属性是否为对象所拥有



https://www.xamrdz.com/lan/58q1924449.html

相关文章: