前言
类型操作,其实就是把一种类型经过一定的逻辑,转换成另一种类型。个人认为这也是 ts 的魅力所在。本篇罗列了所有可分类的类型操作,最后则实现了一遍 ts 内置工具方法,有些方法会写和 ts 内建方法的区别及思路。
typeof
js 中本就有 typeof操作符,用来获取数据的类型(一个 js 的字符串),ts 则加强了它,可以尽量推断出该数据的类型。
注意typeof后面接的是一个 js 变量或者函数。
            
            
              ts
              
              
            
          
          let obj = { name: "", age: 0 };
const objType = typeof obj; // js常量 'object'
type ObjType = typeof obj; // ts类型 { name: string, age: number }
        泛型
泛型可以说是 ts 类型操作的基石。
什么是泛型
- 泛型可以理解成 ts 中的不确定类型或者变量类型,那么包含泛型的类型,就可以叫做泛型类型。 泛型和泛型类型 之间的关系就像 函数形参和函数 的关系。
 - 泛型可以应用在 ts 所有类型系统中,函数,接口,类型别名,类。
 - 在定义泛型类型时,可以通过
<>传入一个变量类型,在应用泛型类型时再精确这个变量类型。 - 泛型类型可以理解成是一个模板,配合传入具体类型,可以解决很多复用性问题。
 
例如,在和后端做数据交互时,后端接口返回的数据都有code, msg, data三个字段,其中 code 和 msg 的类型是固定的,data 则是根据接口含义不同,返回不同的数据。那么用泛型就可以很方便的表示
            
            
              ts
              
              
            
          
          interface FetchResponse<D> {
  // 这个D只是一个变量代指,和形参一样,可以写成任意名称
  code: number;
  msg: string;
  data: D;
}
// 根据接口类型,复用泛型类型即可。
const pageData: FetchResponse<{ page: number; list: string[] }>;
/**
 *{
 * code: number;
 * msg: string;
 * data: { page: number; list: string[] };
 *}
 */
const checkData: FetchResponse<boolean>;
/**
 *{
 * code: number;
 * msg: string;
 * data: boolean;
 *}
 */
        泛型的几种写法
- 函数泛型 
function fn<T>(): T {};,写函数泛型时<>必须紧跟着形参的小括号。 - 函数变量 
const fn = <T>(arg: T) => arg;,写函数泛型时<>必须紧跟着形参的小括号。 - 类型别名泛型 
type UnionType<T1, T2> = T1 | T2; - 接口泛型 
interface I1<T> { t: T } - 类泛型 
class Parent<T> { t: T } 
泛型约束 extends
- 就像在函数体中需要使用参数做一些操作一样,在一些条件下,我们也需要对泛型进行一些操作,例如循环,访问成员类型等,这个时候就需要对泛型进行一定的约束。
 - 如果不进行泛型约束的话,泛型会被赋予类似
unknown的类型,不允许任何操作。 - 泛型约束针对编写者,如果没写泛型约束,泛型就不可以操作;泛型约束同时也针对调用者,如果写了泛型约束,传入的参数必须符合泛型约束。
 - 泛型约束通过
extends关键字实现。 
            
            
              ts
              
              
            
          
          /**
 * From T, pick a set of properties whose keys are in the union K
 */
type Pick<T, K extends keyof T> = {
  [P in K]: T[P];
};
interface Obj {
  a: string;
  b: boolean;
  c: number;
  d: number[];
}
// 这里Pick的第二个参数,就需要约束为 Obj的key 的 联合类型的子类型
type NObj = Pick<Obj, "a" | "b" | "c">;
/*
type NObj = {
    a: string;
    b: boolean;
    c: number;
}
*/
type NObj2 = Pick<Obj, "ae" | "c">; //报错❌,ae 不是 Obj的key
type GetA<T> = T["a"]; // 报错❌,这里T可以理解成 unknown
        泛型参数默认类型 =
- 和函数的形参默认参数类似,泛型参数也可以有默认类型。语法也一样,都是使用
=赋予默认值。 - 泛型参数默认类型是针对调用者的限制 ,即当无法推导出 具有默认类型的泛型参数的类型时,该泛型将限制调用者传入的类型符合默认类型。如果没写泛型约束,对于编写者来说,哪怕参数有默认类型,仍然得当
unknown用。 - 如果一个泛型参数有默认类型,那么该参数是可选的,和 js 函数可选参数一样,其只能排在不可选参数后面。
 
            
            
              ts
              
              
            
          
          interface A<T = string> {
  name: T;
}
const strA: A = { name: "aaa" }; // ok,A的泛型参数可以省略,会被推到为 A<string>
const numB: A = { name: 123 }; // 报错❌,这里A会被推导为A<string>
const numC: A<number> = { name: 123 }; // ok,这里A会被推导为A<number>
        索引访问
和对象可以通过 key 访问 value 一样,ts 也可以通过类型的 key,访问类型的 value
            
            
              ts
              
              
            
          
          type Person = { age: number; name: string; alive: boolean };
type Age = Person["age"]; // number
        ts 除了可以单一索引访问,还支持联合索引访问
            
            
              ts
              
              
            
          
          type Person = { age: number; name: string; alive: boolean };
type I1 = Person["age" | "name"]; //string | number
type I2 = Person[keyof Person]; // string | number | boolean
type AliveOrName = "alive" | "name";
type I3 = Person[AliveOrName]; // string | boolean
        数组和元组存储的成员变量需要使用数字索引访问,它们也支持使用number来访问
            
            
              ts
              
              
            
          
          type Arr = string[];
type Tuple = [string, number];
type ArrItem0 = Arr[0]; // string
type ArrItem = Arr[number]; // string
type Tuple0 = Tuple[0]; // string
type TupleValues = Tuple[number]; // string | number
        ts 中的类型遍历
ts 中可以遍历的类型只有两种,对象类型 和 联合类型。其中数组和元组算是特殊的对象,不过遍历方式和普通对象其实是一样的。
keyof(ts 独有概念)
keyof的作用,就是获取对象类型数据的 key 的联合类型( 把对象类型转换成原始类型的联合类型 )也可以理解成遍历对象类型。当然,用在原始类型上也不会报错,但是没有什么意义,因为会被当做对象处理。
            
            
              ts
              
              
            
          
          type Point = { x: number; y: number };
type P = keyof Point; // 'x' | 'y'
        有一个特殊需要注意的点是,当对象类型包含索引类型,且索引为 string 类型时,因为obj[0]和obj['0']访问是等效的,所以其keyof操作后的结果要多联合一个number类型。
            
            
              ts
              
              
            
          
          type Mapish = { [k: string]: boolean };
type M = keyof Mapish; // string | number
        对象类型的值的联合类型,通过 索引访问 和 keyof 可以搞定
            
            
              ts
              
              
            
          
          type ValueOf<T> = T[keyof T];
type Obj = { a: sting; b: number };
type ValueOfObj = ValueOf<Obj>; // string | number
        keyof优先级高于|和&
            
            
              ts
              
              
            
          
          type A1 = keyof { a: string; b: number } | { a: string };
// => 'a' | 'b' | { a: string }
type A2 = keyof ({ a: string; b: number } | { a: string });
// => keyof { a: string }
// => 'a'
        in
in 关键字也是 js 中原本就有的概念,可以判断某个 key 是否在一个对象中,类似instanceof在 ts 中可以用来做类型保护。当然还有for...in...遍历数组索引,这里就不展开了。
            
            
              ts
              
              
            
          
          let obj = { name: "", age: 0 };
const isInObj = (key: string | number, obj: object) => key in obj;
const res1 = isInObj("name", obj); // true
const res2 = isInObj("anyKey", obj); // false
        上述其实并不属于类型操作,只是 js 中in关键字的用法,在 ts 类型操作中,in关键字则是用来遍历联合类型,生成对象类型的。
- 用在对象类型 key 的部分
 - 用
[]包裹, - 其左侧是一个新的类型,代指联合类型中单个的子项(eg: 
P) - 右侧则是要遍历的联合类型
 - 整个语句都可以使用这个新定义的类型 
P 
            
            
              ts
              
              
            
          
          type Record<K extends keyof any, T> = {
  [P in K]: T;
};
type Point = { x: number; y: number };
type Obj = Record<"x" | "y", Point>; // { x: Point; y: Point; }
type Pick<T, K extends keyof T> = {
  [P in K]: T[P];
};
type ObjX = Pick<Point, "x">; // { x: number; }
        借助in和keyof,可以实现对象类型和联合类型的相互转化
            
            
              ts
              
              
            
          
          interface Obj {
  a: string;
  b: number;
}
type ObjKeys = keyof Obj; // 转联合类型
type ObjN = {
  // 转回对象类型
  [k in ObjKeys]: Obj[k];
};
        as
as关键字一开始是用来做类型断言的,其前面是 js 变量,后面则是推断的 ts 类型。
            
            
              ts
              
              
            
          
          let obj = {} as { a: string };
let obj2 = {} as unknown as string;
        在 ts 4.1 版本之后,as关键字可以应用在映射类型的 key 子句中,创造一个新的子句。其左侧是原始 key ,其右侧为新的 key。
借助as关键字,可以实现in关键字遍历复杂类型的联合类型
            
            
              ts
              
              
            
          
          type EventConfig<Events extends { kind: string }> = {
  //原始key   as    新key
  [E in Events as E["kind"]]: (event: E) => void;
};
type SquareEvent = { kind: "square"; x: number; y: number };
type CircleEvent = { kind: "circle"; radius: number };
type Config = EventConfig<SquareEvent | CircleEvent>;
/* 
=>
{
    square: (event: SquareEvent) => void;
    circle: (event: CircleEvent) => void;
}
*/
        翻转对象类型的 key 和 value
            
            
              ts
              
              
            
          
          type Flip<T extends { [key: string]: string | number }> = {
  [K in keyof T as T[K]]: K;
};
type Obj = { name: "jeffery"; age: 18 };
type FlipObj = Flip<Obj>; // { jeffery: "name";18: "age"; }
        对象类型转联合类型
            
            
              ts
              
              
            
          
          // 处理成嵌套对象,然后通过访问 keyof T ,一次性访问所有key,从而转成联合类型。
type ToUnion<T> = {
  [K in keyof T]: { [P in K]: T[K] };
}[keyof T];
type MyObj = { a: 1; b: 2; c: 3 };
type MyUnion = ToUnion<MyObj>; //  {a: 1;} | {b: 2;} | {c: 3;}
        条件类型 extends
在 ts 中,我们通过extends关键字来实现条件判断语句,extends基于继承这一层关系,在 ts 中总共有三种用途,分别是 继承,泛型约束和条件判断。
            
            
              ts
              
              
            
          
          type ConditionalType = SomeType extends OtherType
  ? TrueBranchType
  : FalseBranchType;
        如果符合前面的继承条件SomeType extends OtherType,则新类型ConditionalType是TrueBranchType,否则是FalseBranchType。
甚至可以说 ts 通过判断两个类型的继承关系实现了条件语句
            
            
              ts
              
              
            
          
          type IsChild = { a: string } extends {} ? true : false; // true
        分布式条件类型
分布式条件类型是指,在 extends 关键字前面是联合类型的裸泛型参数 时,会隐式 将联合类型的每个子类型都应用到判断语句中得到一个子结果,最后再联合这些子结果得到一个新的联合类型 。
特别注意的是,分布式条件类型在全符合以下四种情况时才会触发
- 仅限于泛型参数
 - 该泛型参数出现在
extends关键字的前面(即只分配extends前面的内容) - 该泛型参数是未经处理的,裸类型
 - 该泛型参数是联合类型
 
            
            
              ts
              
              
            
          
          type Is1 = "a" | "b" extends "a" ? true : false; // false,不是泛型参数,并不会触发分布式
type Is2<T> = keyof T extends "a" ? true : false;
type Is2Res = Is2<{ a: string } | { b: number }>; // true ,T在执行 extends前被keyof处理了(处理成了never),不是裸类型了。
type Is3<T> = T extends "a" ? true : false;
type Is3Res1 = Is3<"a" | "b">; //true | false => boolean ,符合隐式触发分布式的情况
        这里有个特例就是never,never类型被认为是空的联合类型。
            
            
              ts
              
              
            
          
          type IsNever<T> = T extends never ? true : false;
type IsNever1 = IsNever<never>; // never
        主要原因是,这里never作为联合类型,符合了触发分步式类型的四个条件(extends关键字前的联合类型裸泛型参数),但是,在进行分发的时候,由于 never 是空的,不可以分发,导致 T extends never语句不可执行,于是就返回了T,即never。
验证是否为never直接破除其四个条件中的一个即可:
            
            
              ts
              
              
            
          
          type IsNever<T> = [T] extends [never] ? true : false;
type IsNever1 = IsNever<never>; // true
        infer
infer是在 ts 2.8 版本中新增的关键字,定义在extends右侧子句内,只能使用在条件判断语句的TrueBranchType中,表示根据TrueBranch推断出一个新的类型。
常见的类型解包
            
            
              ts
              
              
            
          
          // 数组解包
type GetArrItem<T> = T extends (infer U)[] ? U : never;
type Item = GetArrItem<string[]>; // string
// promise解包
type GetPromiseResolve<T> = T extends Promise<infer U> ? U : never;
type PromiseResolve = GetPromiseResolve<{ a: string }>;
        当推断结果有多种情况时,会自动转为联合类型,或者说 infer 会尽量推导出合适的类型。
            
            
              ts
              
              
            
          
          // 获取对象的value类型
type GetObjValueType<T> = T extends { [key: string]: infer U } ? U : never;
type ObjValueType = GetObjValueType<{ a: string; 1: number; b: boolean }>;
// => string | number | boolean
// 获取 元组或者数组 值的联合类型
type GetArrValueType<T> = T extends { [key: number]: infer U } ? U : never;
type TupleValueType = GetArrValueType<[string, number]>; // string | number
        结合递归,遍历处理数据,翻转元组:
            
            
              ts
              
              
            
          
          type ReverseArray<T extends unknown[]> = T extends [infer First, ...infer Rest]
  ? [...ReverseArray<Rest>, First]
  : T;
type Value = ReverseArray<[1, 2, 3, 4]>; // [4,3,2,1]
        结合函数参数逆变,分布式条件类型和 infer 推断的特性,把联合类型转成交叉类型:
            
            
              ts
              
              
            
          
          type UnionToIntersection<T> = (
  T extends unknown ? (a: T) => unknown : never
) extends (arg: infer R) => unknown
  ? R
  : never;
type Example = UnionToIntersection<{ a: string } | { b: number }>; // { a: string } & { b: number }
        - 
第一步,通过条件分布式,把联合类型转变成多个联合的函数,参数为原联合项。
tstype ToUnionFunc<U> = U extends unknown ? (a: U) => unknown : never; type T1 = ToUnionFunc<{ a: 1 } | { b: 2 }>; // ((a: { a: 1 }) => unknown) | ((a: { b: 2 }) => unknown) - 
第二步,借助函数被替换或继承时参数是逆变的,与 infer 会尽量推导出合理的结果这两个特性,实现多个联合函数参数到单一函数交叉参数的目的。
带到当前逻辑中,
extends右侧的单一函数的参数需要符合是extends左侧联合函数的每个参数的(即例子中的{ a: string }和{ b: number })的子类型,当前推断才可以成立。而所有联合项的子类型,其实就是所有联合项的交叉类型(如果不理解,可以参考从集合树的角度理解 ts 不同类型之间的关系),
infer可以很容易推断出这个结果。最后再把推断出来的交叉参数返回即可。ts// 这里记得通过元组,破一下隐式触发条件分布式 type UnionFunToIntersectionArg<UnionFunc> = [UnionFunc] extends [ (arg: infer U) => unknown ] ? U : never; type T1 = ((a: { a: 1 }) => unknown) | ((a: { b: 2 }) => unknown); type T2 = UnionFunToIntersectionArg<T1>; // { a: 1 } & { b: 2 }; 
模板字符串类型
模版字符串类型的语法和 js 模版字符串的语法是一致的。
            
            
              ts
              
              
            
          
          type World = "world";
type Greeting = `hello ${World}`; // "hello world"
        当联合类型用在字符串类型的变量位置时,ts 会将所有可能的类型推断出来,并联合,其实也可以理解成另外一个分布式:
            
            
              ts
              
              
            
          
          type male = "梁山伯" | "Romeo";
type female = "祝英台" | "Juliet";
type song = `${male}和${female}`;
// => "梁山伯和祝英台" | "梁山伯和Juliet" | "Romeo和祝英台" | "Romeo和Juliet"
        有了字符串模板类型,ts 就突破了只能依赖原始类型的限制,开始能够创建类型。
为对象类型添加getter和setter方法:
            
            
              ts
              
              
            
          
          interface Person {
  name: string;
  age: number;
  handleSay: () => string;
}
type AndGetter<T extends object> = {
  [p in keyof T as T[p] extends Function
    ? never
    : p extends string
    ? `get${Capitalize<p>}`
    : never]: () => T[p];
};
type AndSetter<T extends object> = {
  [p in keyof T as T[p] extends Function
    ? never
    : p extends string
    ? `set${Capitalize<p>}`
    : never]: (arg: T[p]) => void;
};
type NP = Person & AndGetter<Person> & AndSetter<Person>;
/* 
{
  name:string;
  age:number;
  getName: () => string;
  getAge: () => number;
  setName: (arg:string) => void;
  setAge: (arg:number) => void;
}
*/
        上面其实用到了一个特殊的类型操作Capitalize,这个是 ts 编辑器内置的四个字符串操作类型之一。它们分别是:
- 全字母大写 
Uppercase<StringType> - 全字母小写 
Lowercase<StringType> - 首字母大写 
Capitalize<StringType> - 首字母小写 
Uncapitalize<StringType> 
映射类型
映射类型其实就是通过上述的各种操作,把一个对象类型,转换成另外一种类型。其实上面涉及对象类型转换的,都属于映射类型,由于有上面比较丰富详细的示例,它没有必要单独拎出来,再重复一遍了。
ts 内置类型操作
这些内置类型,其实很方便,尤其是在多参数的情况下,我个人很容易不记得哪个是 Key,哪个是 Type,那个是 Union,所以,在这里写的时候,我会尽量总结分个类
对象类型操作
- 
Partial<Type>,将对象类型所有键都变成可选的。tstype Partial<Type> = { [P in keyof Type]?: Type[P]; }; - 
Required<Type>,和Partial相反,将所有键变成必填tstype Required<Type> = { [P in keyof Type]-?: Type[P]; }; - 
Readonly<Type>,将所有键变成只读tstype Readonly<Type> = { readonly [P in keyof Type]: Type[P]; }; - 
Record<Keys, Value>,创建一个对象类型,键是Keys里的每一项,值是Value。这里加了一个泛型约束,就是Keys是keyof any的子类型。保证能被in关键字处理tstype Record<Keys extends keyof any, Value> = { [k in Keys]: Value; }; - 
Pick<Type, Keys>,从Type中挑选联合类型Keys相关的键值对,组合新的对象类型tstype Pick<Type, Keys extends keyof Type> = { [P in Keys]: Type[P]; }; - 
Omit<Type, Keys>,忽略Type中的Keys相关键值对,组合新的对象类型。通过never过滤在Type中的Keys,个人感觉叫Filter更好理解。ts// 内置实现 type Omit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>; // 手动实现 type Om<Type, Keys> = { [k in keyof Type as k extends Keys ? never : k]: Type[k]; }; // 示例 interface Todo { title: string; description: string; completed: boolean; createdAt: number; } type TodoPreview = Om<Todo, "title" | "description">; // {completed: boolean; createdAt: number;} 
联合类型操作
- 
Extract<UnionType, Members>,从UnionType中提取Members,和对象类型的Pick很相近,我经常搞混,总觉得叫Include比较合理tstype Extract<UnionType, Members> = Members extends UnionType ? Members : never; - 
Exclude<UnionType, Members>,从UnionType中忽略Members,返回一个新的联合类型,其实和对象操作的Omit很相近,我也经常搞不清楚这俩。主要借助了条件分布式和 never 的特性。值得注意的是,其实要过滤
UnionType中的每一项,所以条件语句得是UnionType extends Memberststype Exclude<UnionType, Members> = UnionType extends Members ? never : UnionType; 
函数相关操作
- 
Parameters<Type>,获取函数类型的参数tstype Parameters<Type extends Function> = Type extends ( ...args: infer Params ) => unknown ? Params : never; - 
ReturnType<Type>,获取函数类型的返回值tstype ReturnType<Type extends Function> = Type extends ( ...args: never ) => infer R ? R : never; - 
ConstructorParameters<ClassType>,获取构造函数的参数类型。ClassType是类的类型,其实在 ts 里,它本身就是构造函数,和推断函数参数比起来,只是加了new关键字而已tstype ConstructorParameters< ClassType extends new (...args: never) => unknown > = ClassType extends new (...args: infer Params) => unknown ? Params : never; // 示例 class C { constructor(a: number, b: string) {} } type T3 = ConstructorParameters<typeof C>; // [a: number, b: string] - 
InstanceType<ClassType>,获取示例类型,其实就是构造函数的返回值。tstype InstanceType<ClassType extends new (args: never) => unknown> = ClassType extends new (...args: never) => infer R ? R : never; // 示例: class C { x = 0; y = 0; } type T0 = InstanceType<typeof C>; // C - 
ThisParameterType<FnType>,获取函数中 this 的类型。ts 函数中,有一个假参数 this,可以指定当前函数中的this类型,只要用infer推断出这个this加参数的类型即可。tstype ThisParameterType<FnType extends Function> = FnType extends ( this: infer T, ...args: never ) => unknown ? T : never; // 示例 function toHex(this: Number) { return this.toString(16); } type thisT = ThisParameterType<typeof toHex>; // Number - 
OmitThisParameter<Type>,获取忽略 this 之后的这个函数类型。因为 this 类型需要明确指出,展开运算符无法包含this类型,所以使用展开运算符处理参数,即可忽略this的类型。ts// 官方实现 type OmitThisParameter<T> = unknown extends ThisParameterType< (this: string, age: number) => number > ? T : T extends (...args: infer A) => infer R ? (...args: A) => R : T; // 自己实现: type OmitThisParameter<FnType extends Function> = FnType extends ( ...args: infer T ) => infer R ? (...args: T) => R : never; // 示例 type F1 = OmitThisParameter<(this: string, age: number) => number>; type F2 = OmitThisParameter<(age: number) => number>; // (age: number) => number - 
ThisType<Type>,指定对象上下文中的 this 类型。记得 tsconfig 里配置"noImplicitThis": true。个人感觉它的应用场景不多,很重要的一点是因为,如果一个对象类型指定了 this 类型,那对象中所有的 key-value 类型就都得显式指定了。tsconst obj2: ThisType<{ foo1: string; bar(): void }> & any = { foo: "Hello", bar() { console.log(this.foo1); // this: { foo1: string; bar(): void } }, }; obj.foo; // 报错,因为只显示指定了this,没有指定其他类型。目前看到官网的实例很好,是通过函数参数泛型推导来解决显示类型指定的问题
tstype ObjectDescriptor<D, M> = { data?: D; methods?: M & ThisType<D & M>; // Type of 'this' in methods is D & M }; function makeObject<D, M>(desc: ObjectDescriptor<D, M>): D & M { let data: object = desc.data || {}; let methods: object = desc.methods || {}; return { ...data, ...methods } as D & M; } let obj = makeObject({ data: { x: 0, y: 0 }, methods: { moveBy(dx: number, dy: number) { this.x += dx; // Strongly typed this this.y += dy; // Strongly typed this }, }, }); obj.x = 10; obj.y = 20; obj.moveBy(5, 5); - 
Awaited,递归解包 promise 类型,直到最终解析的结果。ts// 这个是ts内置实现,主要判断了thenable对象等多种情况,需要通过onfulfilled参数获取结果。 type Awaited<T> = T extends null | undefined ? T // special case for `null | undefined` when not in `--strictNullChecks` mode : T extends object & { then(onfulfilled: infer F, ...args: infer _): any } // `await` only unwraps object types with a callable `then`. Non-object types are not unwrapped ? F extends (value: infer V, ...args: infer _) => any // if the argument to `then` is callable, extracts the first argument ? Awaited<V> // recursively unwrap the value : never // the argument to `then` was not callable : T; // non-object or non-thenable // 简版 type SimpleAwaited<T> = T extends Promise<infer U> ? SimpleAwaited<U> : T; type Res = SimpleAwaited<Promise<Promise<number>>>; // number 
其他
- 
NonNullable<Type>,排除 null 和 undefined,主要是借助下{}类型和交叉类型的特性tstype NonNullable<Type> = Type & {};