ts类型操作大全
前言
类型操作,其实就是把一种类型经过一定的逻辑,转换成另一种类型。个人认为这也是 ts 的魅力所在。本篇罗列了所有可分类的类型操作,最后则实现了一遍 ts 内置工具方法,有些方法会写和 ts 内建方法的区别及思路。
typeof
js 中本就有 typeof
操作符,用来获取数据的类型(一个 js 的字符串),ts 则加强了它,可以尽量推断出该数据的类型。
注意typeof
后面接的是一个 js 变量或者函数。
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 则是根据接口含义不同,返回不同的数据。那么用泛型就可以很方便的表示
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
关键字实现。
/**
* 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 函数可选参数一样,其只能排在不可选参数后面。
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
type Person = { age: number; name: string; alive: boolean };
type Age = Person["age"]; // number
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
来访问
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 的联合类型( 把对象类型转换成原始类型的联合类型 )也可以理解成遍历对象类型。当然,用在原始类型上也不会报错,但是没有什么意义,因为会被当做对象处理。
type Point = { x: number; y: number };
type P = keyof Point; // 'x' | 'y'
有一个特殊需要注意的点是,当对象类型包含索引类型,且索引为 string 类型时,因为obj[0]
和obj['0']
访问是等效的,所以其keyof
操作后的结果要多联合一个number
类型。
type Mapish = { [k: string]: boolean };
type M = keyof Mapish; // string | number
对象类型的值的联合类型,通过 索引访问 和 keyof
可以搞定
type ValueOf<T> = T[keyof T];
type Obj = { a: sting; b: number };
type ValueOfObj = ValueOf<Obj>; // string | number
keyof
优先级高于|
和&
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...
遍历数组索引,这里就不展开了。
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
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
,可以实现对象类型和联合类型的相互转化
interface Obj {
a: string;
b: number;
}
type ObjKeys = keyof Obj; // 转联合类型
type ObjN = {
// 转回对象类型
[k in ObjKeys]: Obj[k];
};
as
as
关键字一开始是用来做类型断言的,其前面是 js 变量,后面则是推断的 ts 类型。
let obj = {} as { a: string };
let obj2 = {} as unknown as string;
在 ts 4.1 版本之后,as
关键字可以应用在映射类型的 key 子句中,创造一个新的子句。其左侧是原始 key ,其右侧为新的 key。
借助as
关键字,可以实现in
关键字遍历复杂类型的联合类型
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
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"; }
对象类型转联合类型
// 处理成嵌套对象,然后通过访问 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 中总共有三种用途,分别是 继承,泛型约束和条件判断。
type ConditionalType = SomeType extends OtherType
? TrueBranchType
: FalseBranchType;
如果符合前面的继承条件SomeType extends OtherType
,则新类型ConditionalType
是TrueBranchType
,否则是FalseBranchType
。
甚至可以说 ts 通过判断两个类型的继承关系实现了条件语句
type IsChild = { a: string } extends {} ? true : false; // true
分布式条件类型
分布式条件类型是指,在 extends 关键字前面是联合类型的裸泛型参数 时,会隐式 将联合类型的每个子类型都应用到判断语句中得到一个子结果,最后再联合这些子结果得到一个新的联合类型 。 特别注意的是,分布式条件类型在全符合以下四种情况时才会触发
- 仅限于泛型参数
- 该泛型参数出现在
extends
关键字的前面(即只分配extends
前面的内容) - 该泛型参数是未经处理的,裸类型
- 该泛型参数是联合类型
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
类型被认为是空的联合类型。
type IsNever<T> = T extends never ? true : false;
type IsNever1 = IsNever<never>; // never
主要原因是,这里never
作为联合类型,符合了触发分步式类型的四个条件(extends
关键字前的联合类型裸泛型参数),但是,在进行分发的时候,由于 never 是空的,不可以分发,导致 T extends never
语句不可执行,于是就返回了T
,即never
。
验证是否为never
直接破除其四个条件中的一个即可:
type IsNever<T> = [T] extends [never] ? true : false;
type IsNever1 = IsNever<never>; // true
infer
infer
是在 ts 2.8 版本中新增的关键字,定义在extends
右侧子句内,只能使用在条件判断语句的TrueBranchType
中,表示根据TrueBranch
推断出一个新的类型。
常见的类型解包
// 数组解包
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 会尽量推导出合适的类型。
// 获取对象的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
结合递归,遍历处理数据,翻转元组:
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
推断的特性,把联合类型转成交叉类型:
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 }
模板字符串类型
模版字符串类型的语法和 js 模版字符串的语法是一致的。
type World = "world";
type Greeting = `hello ${World}`; // "hello world"
当联合类型用在字符串类型的变量位置时,ts 会将所有可能的类型推断出来,并联合,其实也可以理解成另外一个分布式:
type male = "梁山伯" | "Romeo";
type female = "祝英台" | "Juliet";
type song = `${male}和${female}`;
// => "梁山伯和祝英台" | "梁山伯和Juliet" | "Romeo和祝英台" | "Romeo和Juliet"
有了字符串模板类型,ts 就突破了只能依赖原始类型的限制,开始能够创建类型。
为对象类型添加getter
和setter
方法:
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>
,将对象类型所有键都变成可选的。type Partial<Type> = { [P in keyof Type]?: Type[P]; };
-
Required<Type>
,和Partial
相反,将所有键变成必填type Required<Type> = { [P in keyof Type]-?: Type[P]; };
-
Readonly<Type>
,将所有键变成只读type Readonly<Type> = { readonly [P in keyof Type]: Type[P]; };
-
Record<Keys, Value>
,创建一个对象类型,键是Keys
里的每一项,值是Value
。这里加了一个泛型约束,就是Keys
是keyof any
的子类型。保证能被in
关键字处理type Record<Keys extends keyof any, Value> = { [k in Keys]: Value; };
-
Pick<Type, Keys>
,从Type
中挑选联合类型Keys
相关的键值对,组合新的对象类型type Pick<Type, Keys extends keyof Type> = { [P in Keys]: Type[P]; };
-
Omit<Type, Keys>
,忽略Type
中的Keys
相关键值对,组合新的对象类型。通过never
过滤在Type
中的Keys
,个人感觉叫Filter
更好理解。// 内置实现 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
比较合理type Extract<UnionType, Members> = Members extends UnionType ? Members : never;
-
Exclude<UnionType, Members>
,从UnionType
中忽略Members
,返回一个新的联合类型,其实和对象操作的Omit
很相近,我也经常搞不清楚这俩。 主要借助了条件分布式和 never 的特性。值得注意的是,其实要过滤UnionType
中的每一项,所以条件语句得是UnionType extends Members
type Exclude<UnionType, Members> = UnionType extends Members ? never : UnionType;
函数相关操作
-
Parameters<Type>
,获取函数类型的参数type Parameters<Type extends Function> = Type extends ( ...args: infer Params ) => unknown ? Params : never;
-
ReturnType<Type>
,获取函数类型的返回值type ReturnType<Type extends Function> = Type extends ( ...args: never ) => infer R ? R : never;
-
ConstructorParameters<ClassType>
,获取构造函数的参数类型。ClassType
是类的类型,其实在 ts 里,它本身就是构造函数,和推断函数参数比起来,只是加了new
关键字而已type 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>
,获取示例类型,其实就是构造函数的返回值。type 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
加参数的类型即可。type 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
的类型。// 官方实现 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 类型就都得显式指定了。const obj2: ThisType<{ foo1: string; bar(): void }> & any = { foo: "Hello", bar() { console.log(this.foo1); // this: { foo1: string; bar(): void } }, }; obj.foo; // 报错,因为只显示指定了this,没有指定其他类型。
目前看到官网的实例很好,是通过函数参数泛型推导来解决显示类型指定的问题
type 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内置实现,主要判断了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,主要是借助下{}
类型和交叉类型的特性type NonNullable<Type> = Type & {};
参考资料
转载自:https://juejin.cn/post/7311602698721001522