TypeScript 高级类型
类型简介
TypeScript 包含了 JavaScript 的 8 种基本类型:
- number
- boolean
- object
- string
- bigint
- symbol
- undefined
- null
还有复合类型:
- Array
- class
- Promise
- ...
另外增加了三种复合类型:
- Tuple(元组)
- interface(接口)
- Enum(枚举)
以及四种特殊类型:
-
void :表示为空,一般用来表示返回值为 undefined
-
never:表示永不可达的类型
-
any:任意类型
-
unknown:未知类型,任何类型都可以赋值给它,但是它不可以赋值给别的类型。
为什么说任何类型都可以赋值给 unknown 呢?比如以下代码,我们可以把任何类型的数据传给
getSomething函数。function getSomething(x:unknown){ return x }然而,一旦把它认定成别的类型,就会报错。

我们需要通过类型收窄来使得 unknown 类型更加安全
js1class Person { 2 name: string = 'qyx' 3} 4 5function getSomething(x: unknown) { 6 if (x instanceof Person) return x.name 7 return 'something' 8}
同时,可以用 type来做类型别名。
type 和 interface 的区别
编译器对 type 更友好,通常能够在鼠标悬浮时直接显示 type 的字面量类型
而 interface 则并没有那么友好
type 是类型别名,interface 是接口。类型别名不能
extends和implements,如果想要类似extends的效果只能用&运算符交叉合并类型。而
&的效果跟extends也是有差别的,比如同属性情况下用&会得到 never 类型
而用
extends就可以得到报错信息
interface 没有办法写元组类型或者联合类型。
同名的 interface 会合并,同名的 type 会报错。
js1interface User { 2 name: string; 3} 4interface User { 5 age: number; 6} 7let user: User = { 8 name: 'qiuyanxi', 9 age: 18, 10}js1type Admin = { 2 name: string, 3} 4type Admin = { 5 age: number, 6} // ❎ 报错啦
Tuple
元组(Tuple)就是元素的个数和类型固定的数组类型:
js1type Tuple = [number, string] 2let array: Tuple = [1, 2, '2'] // ❎ 长度和类型不对 3let array: Tuple = [1, '2'] // ✅
interface
接口(interface)可以用来描述函数、对象、构造器的结构,比如我需要描述对象:
js1interface IPerson { 2 name: string; 3 age: number; 4} 5 6class Person implements IPerson { 7 name: string 8 age: number 9} 10 11const obj: IPerson = { 12 name: 'guang', 13 age: 18, 14}
描述函数:
js1interface SayHi { 2 (name: string): string; 3} 4const sayHi: SayHi = (name: string) => { 5 return `hi ${name}` 6}
描述构造器:
js1interface PersonConstructor { 2 new(name: string, age: number): IPerson; 3} 4 5function createPerson(ctor: PersonConstructor): IPerson { 6 return new ctor('guang', 18) 7}
对象类型、class 类型也叫做索引类型,也就是这几个类型索引了多个元素的类型的意思。
如果一个对象想要动态添加属性,但是又可以是任意属性,那么就使用索引签名:
js1interface IPerson { 2 [prop: string]: string | number; // 索引签名 3} 4const obj: IPerson = {} 5obj.name = 'qiuyanxi' 6obj.age = 18
Enum
枚举是一系列值的复合,可以用枚举来敲定常量
js1enum Transpiler { 2 Babel = 'babel', 3 Postcss = 'postcss', 4 Terser = 'terser', 5 Prettier = 'prettier', 6 TypeScriptCompiler = 'tsc' 7} 8 9const transpiler = Transpiler.TypeScriptCompiler;
字符串字面量
TypeSctipt还支持字面量类型,也就是可以拿 123、’123‘等这些值来做类型。
其中字符串字面量支持普通字符串字面量还有模板字面量。
如果我们希望指定一个以#开头后面是任意string的字符串,那么可以使用模板字面量
js1const hello: "hello" = "hello"; // 普通字面量 2const temp:`#${string}`='#123' // 模板字面量
类型装饰
除了描述类型的结构外,TypeScript 还支持描述类型的属性,比如属性是否是可选的,是否是只读的:
js1interface IPerson { 2 readonly name: string; 3 age?: number; 4} 5 6type tuple = [string, number?];
类型运算
条件类型
条件类型用来做一些动态的类型计算。比如以下例子:
js1type isTwo<T> = T extends 2 ? true: false; 2 3type res = isTwo<1>; // false类型 4type res2 = isTwo<2>; // true 类型
这种类型也叫做高级类型。
高级类型的特点是传入类型参数,经过一系列类型运算逻辑后,返回新的类型。
infer 推导
infer可以表示在 extends 条件语句中待推断的类型变量。
假设现在我需要两个类型,这两个类型分别是从两个对象的属性中提取出来的,而不是静态定义的类型:
js1const user = { name: 'qiuyanxi', age: '12' } 2const admin = { name: 'qiuyanxi', age: 12 }
我需要推导出 age 属性的两个类型,分别是 string 和 number,那么我应该这么做:
js1type AgeTypeOf<T> = T extends { age: infer P } ? P : unknown; 2 3type UserAgeType = AgeTypeOf<typeof user>; // string 4type AdminAgeType = AgeTypeOf<typeof admin>; // number
如果想要收窄泛型 T ,那么还可以这么做:
js1type AgeTypeOf<T extends { name: string }> = T extends { age: infer P } 2 ? P 3 : unknown; 4 5type UserAgeType = AgeTypeOf<typeof user>; 6type AdminAgeType = AgeTypeOf<typeof admin>; 7 8const noName = { age: 12 }; 9type noName = AgeTypeOf<typeof noName>; // Type '{ age: number; }' does not satisfy the constraint '{ name: string; }'.
联合类型
联合类型使用|运算符,表示就这么几个类型之一:
js1type Union = 1 | 2 | 3
联合类型可以用来取并集或者其中一个类型用
js1type A = { name: string } 2type B = { age: number } 3type C = A | B 4 5const person1: C = { 6 name: 'qiuyanxi', 7} 8 9const person2: C = { 10 age: 123, 11} 12 13const person3: C = { 14 name: 'qiuyanxi', 15 age: 123, 16}
上面的代码中,联合类型既可以用来表示 A,也可以用来表示 B,也可以用来表示 A 与 B 的并集的集合。
类型收窄
类型收窄就是类型可能有很多种情况,我们需要根据不同的情况做不同的处理,这时候需要收窄类型。
- typeof
比如下面的代码
js1function getSomething(a: number | string) { 2 return a.toFixed(2) 3}
由于 a 可能是 number 也可能是 string 类型,这时候a.toFixed就会报错

因为 string 类型是没有 toFixed 这个方法的,于是编译就不通过。
这时候可以用 typeof 来收窄类型
js1function getSomething(a: number | string) { 2 if (typeof a === 'number') { 3 return a.toFixed(2) 4 } else { 5 return a.slice() 6 } 7}
typeof 可以用来收窄以下类型:number、string、function、object、boolean、symbol、undefined。
-
instanceof
同理还有
instanceof语法,经常用于Object等复杂类型js1class Person { 2 count: number = 0 3} 4function addObj(first: object | Person, second: object | Person) { 5 return first.count + second.count 6} // 类型“object”上不存在属性“count”。ts(2339) 7复制代码上面就会报错,我们可以使用
instanceof做一个判断js1class Person { 2 count: number = 0 3} 4function addObj(first: object | Person, second: object | Person) { 5 if (first instanceof Person && second instanceof Person) { 6 return first.count + second.count 7 } 8 return null 9}instanceof常用于 JS 内置对象、或者 class 类型的判断,不能用于type或者interface定义的类型。例如把上面的 Person 修改成 type 类型就会报错
-
in
in语法就是用来解决上面的问题的。我们可以用 in 来做逻辑判断:js1interface women { 2 say(): number; 3} 4interface man { 5 sayHi(): number; 6} 7function fn(arg: women | man) { 8 if ('say' in arg) { 9 arg.say() 10 } else { 11 arg.sayHi() 12 } 13} -
用 isArray 收窄
事实上,上面的 typeof、in、instanceof 都是 js 的方式来完成类型收窄的,TS 能够根据这些收窄方式来判断出不同的类型,所以理论上还有
isArray可以收窄js1function getSomething(a: string | string[]) { 2 if (Array.isArray(a)) { 3 return a.concat([]) 4 } 5 return a.slice() 6} -
is 收窄
is收窄是 用 TS 的方式手动进行收窄,需要用到一个函数:js1type A = { 2 name: string 3} 4 5type B = string[] 6 7function getSomething(a: A | B) { 8 if (isA(a)) { 9 return a.name 10 } else { 11 return a.concat([]) 12 } 13} 14 15function isA(x: A | B): x is A { 16 return 'name' in x 17}我们定义了一个
isA的函数,注意它的返回值需要是x is A而不是boolean。这样 TS 就能识别我们手动收窄的类型。以下是不用 is 关键字的案例,可以看到报错了。

使用 is 后,类型收窄完成,报错消失。

-
as 断言
as 是强制让 ts 认识到这个类型是什么,有时候 ts 没办法区分类型是什么,而我们是知道的,这个时候就可以用 as 断言来强制收窄。

交叉类型
交叉类型使用&运算符,可以对类型进行合并
js1type User = { age: string } & { name: string } 2const user: User = { 3 name: 'qiuyanxi', 4 age: '18', 5}
不同的类型则无法合并:

两个具有相同属性的类型别名合并后的交叉类型是never

映射类型
对象、class 在 TypeScript 中对应的类型是索引类型(Index Type),如果我们想要查询索引类型中所有的索引,可以用索引查询:
let user = { age: 18, name: "qiuyanxi" };
type User = typeof user;
let admin: { [key in keyof User]: User[key] };
keyof User是索引查询User[key]是索引访问in用来遍历联合类型

上面的方式中,我们通过关键字从一个类型的集合映射出另一个集合,这就是映射类型。
通过映射类型,我们可以做很多事,比如,根据User的类型映射出一个全新的类型
js1let user = { age: 18, name: "qiuyanxi" }; 2type User = typeof user; 3type MapType<T> = { 4 [key in keyof T]: [T[key], T[key]]; 5}; 6 7type newType = MapType<User>;

小结
TypeScript 支持条件、推导、联合、交叉、映射等多种类型运算。
经过一系列类型运算后,返回出的新的类型,我们称之为高级类型。
这些语法经过组合通常可以实现很多复杂的逻辑。
模式匹配
TypeScript 的模式匹配非常重要,我们写类型时很多时候并不需要重新写类型,而是基于某个类型来匹配,并从匹配结果中提取出最新的类型。
比如下面有两个函数,我需要提取出它的返回值:
js1function promiseWithNumber() { 2 return Promise.resolve(1); 3} 4function promiseWithString() { 5 return Promise.resolve("1"); 6} 7 8type GetValueType<T> = T extends () => Promise<infer Value> ? Value : unknown; // * 9type ValueTypeIsNumber = GetValueType<typeof promiseWithNumber>; // number 10type ValueTypeIsString = GetValueType<typeof promiseWithString>; // string
第*行的意思是:
- 基于传递进来的泛型进行匹配
- 通过 extends 对传入的类型参数 T 做模式匹配,其中值的类型是需要提取的,通过 infer 声明一个局部变量
Value来保存。infer Value表示Value是待推断的类型变量。 - 如果匹配成功,则返回推导出来的 Value 的类型。如果没成功,则返回 unknown。
总结一下就是:
TypeScript 类型的模式匹配是通过 extends 对类型参数(也就是泛型)做匹配,结果会用infer声明一个待推断的类型变量来保存。如果匹配成功就取出类型变量里面的类型。
从数组中提取类型
现在有一个数组类型,我们希望提取出它的第一个元素和最后一个元素的类型:
js1type arr = [1, 2, 3]
-
提取第一个元素的类型
js1type GetFirst<T extends unknown[]> = T extends [infer First, ...unknown[]] 2 ? First 3 : unknown; 4type First = GetFirst<[1, "2", 3]>;
-
提取最后一个元素的类型
只需要换一下位置即可
js1type GetLast<T extends unknown[]> = T extends [...unknown[], infer Last] 2 ? Last 3 : unknown; 4type Last = GetLast<[1, "2", 3]>; -
提取除第一个元素外的类型
js1type Shift<T extends unknown[]> = T extends [unknown, ...(infer Rest)] 2 ? Rest 3 : unknown; 4type ShiftArr = Shift<[1, "2", 3]>;
-
提取除最后一个元素外的类型
js1type Pop<T extends unknown[]> = T extends [...(infer Rest), unknown] 2 ? Rest 3 : unknown;
从对象中提取类型
我需要分别提取出 User 跟 Admin 的 value 的类型,应该怎么做?
js1const User = { 2 key: 'name', 3 value: 'qiuyanxi', 4} 5const Admin = { 6 key: 'age', 7 value: 12, 8}
从对象中提取类型其实更加简单:
js1type ValueOf<T> = T extends { key: string; value: infer Value } 2 ? Value 3 : unknown; 4 5type UserValueType = ValueOf<typeof User>; 6type AdminValueType = ValueOf<typeof Admin>;
有时候我们的业务促使我们需要写一个 JS 版本的类型 schema(类似于 Vue3):
js1const user = { 2 name: { type: 'string', default: 'qiuyanxi' }, 3 age: { type: 'number', default: 18 }, 4}
往往这一类类型标注表还会对应一套类型,如果我们纯手写,遇到类型很多的情况就会很麻烦:
js1interface User { 2 name: string; 3 age: number; // 可能下面还有一百个要写 4}
分析一下,实际上我们想要达成的类型,它的key都来自于user,value的类型则可以从default推导。
那么我们就可以使用索引查询(keyof)、in 运算符,索引访问、infer 推导来完成这一需求
-
key都来自于user,那就使用索引查询和 in 运算符来定义键的内容js1[key in keyof typeof user] -
value类型可以从 default 中取,那我们就需要用索引访问来访问对应的 key,infer 来推导其 default 的类型js1type ValueOf<T> = T extends { default: infer Value } ? Value : unknown; 2 3type User = { 4 [key in keyof typeof user]: ValueOf<typeof user[key]>; 5};
最后我们可以得出一个映射类型:

从函数提取类型
有一个函数是这样的:
js1const getUser = (args: { name: string, age: number }) => { 2 return args 3}
-
从函数的参数提取类型
现在我需要用到这个函数的参数的类型,则可以用模式匹配将它的参数提取出来
js1type GetParameter<Func> = Func extends (arg: infer Arg) => any ? Arg : any; 2 3type Arg = GetParameter<typeof getUser>;
-
从函数的返回值提取类型
还是同样的函数,我需要从中提取返回值的类型,只需要稍微改一下代码:
js1type GetReturnType<Func> = Func extends (arg: any) => infer Value ? Value : any; 2 3type ReturnValueType = GetReturnType<typeof getUser>;
小结
TypeScript 类型的模式匹配是通过类型 extends 一个模式类型,把需要提取的部分放到通过 infer 声明的局部变量里,后面可以从这个局部变量拿到类型做各种后续处理。
重新构造
TypeScript 类型系统支持 3 种可以声明任意类型的变量: type、infer、类型参数。
type 叫做类型别名,其实就是声明一个变量存储某个类型。
js1type ttt = Promise<number>
infer 用于类型的提取,然后存到一个变量里,相当于局部变量:
js1type GetValueType<P> = P extends Promise<infer Value> ? Value : never;
类型参数用于接受具体的类型,在类型运算中也相当于局部变量:
js1type isTwo<T> = T extends 2 ? true: false;
但是这些都不能重新赋值。
如果我们希望通过这些类型来做各种变换(增删改)就需要重新构造类型。
重新构造索引类型涉及到映射类型的语法。
实现 Readonly
TypeScript 内置 Readonly 关键字,它能够使得索引类型都添加上readonly装饰。
我们用映射类型也能写出来:
js1type toReadonly<T> = { 2 readonly [K in keyof T]: T[K]; 3};
其实源码也是这样的:
js1type Readonly<T> = { readonly [P in keyof T]: T[P]; }
本质上就是传入一个泛型参数,然后用in运算符来遍历它的所有 key(keyof),然后加上readonly。
实现 Partial
同样的,使用映射类型也可以实现Partial关键字的功能。
js1type toPartial<T> = { 2 [K in keyof T]?: T[K]; 3}; 4 5type User = { 6 name: string; 7 age: number; 8}; 9 10type readonlyUser = toPartial<User>;
给索引类型 T 的索引添加了 ? 可选修饰符,其余保持不变。

移除 readonly
我们也可以手写实现移除readonly关键字:
js1type toMutable<T> = { 2 -readonly [K in keyof T]: T[K]; 3}; 4type User = { 5 readonly name: string; 6 readonly age: number; 7}; 8type MutableUser = toMutable<User>;

可以看到移除readonly可以使用-修饰符。
实现 Required
同理,- 也可以实现移除?可选修饰符。
js1type toRequired<T> = { 2 [K in keyof T]-?: T[K]; 3};
实现 Pick
Pick 关键字可以将想要的属性类型拎出来。
js1Pick<User, "name" | "age">;
其实我们要的就是name|age,那么可以通过in来遍历它们而不是遍历 User。
js1type toPick<T, K extends keyof T> = { 2 [P in K]: T[P]; 3};
需要注意的是,K 必须具备的条件是 T 的键,否则T[P]就没有意义,并且会报错。
所以我们必须手动写上extends关键字手动告诉TypeScript 类型系统K来自于keyof T
实现 Record
跟 Pick 差不多的套路。
js1type toRecord<K extends string | number | symbol, V> = { 2 [P in K]: V; 3};
内置的高级类型
Parameters
parameters用于提取函数的参数类型。
源码是这样的:
js1type Parameters<T extends (...args: any) => any> 2 = T extends (...args: infer P) => any 3 ? P 4 : never;
解释一下:
- parameters 的类型参数
T通过extends约束为函数,参数和返回值是any - 通过 extends 匹配一个模式,通过 infer 来提取出 args 的类型并赋值给 P
ReturnType
ReturnType 是提取函数类型的返回值类型。
源码如下:
js1type ReturnType<T extends (...args: any) => any> 2 = T extends (...args: any) => infer R 3 ? R 4 : any;
跟Parameters类似,返回值R的类型被 infer 提取出来,如果匹配成功则将返回值的类型给返回出来。
ConstructorParameters
构造器类型和函数类型的区别就是可以被 new。
Parameters 用于提取函数参数的类型,而 ConstructorParameters 用于提取构造器参数的类型。
源码如下:
js1type ConstructorParameters< 2 T extends abstract new (...args: any) => any 3> = T extends abstract new (...args: infer P) => any 4 ? P 5 : never;
类型参数 T 是待处理的类型,通过 extends 约束为构造器类型,加个 abstract 代表不能直接被实例化(其实不加也行)。
用 T 匹配一个模式类型,提取参数的部分到 infer 声明的局部变量 P 里,返回 P。
这样就实现了构造器参数类型的提取。
InstanceType
InstanceType用来提取构造器返回值的类型。
源码是这样的:
js1type InstanceType< 2 T extends abstract new (...args: any) => any 3> = T extends abstract new (...args: any) => infer R 4 ? R 5 : any;
整体和 ConstructorParameters 差不多,只不过提取的不再是参数了,而是返回值。
ThisParameterType
函数里面可以调用this,这个this的类型也可以约束:

如果想要提取出 this 出来,自然也是可以的。
js1const person = { name: 'qiuyanxi' } 2function sayHi(this: typeof person) { 3 console.log(this.name) 4} 5type ThisRes = ThisParameterType<typeof sayHi>

源码如下:
js1type ThisParameterType<T> = T extends (this: infer U, ...args: any[]) => any 2 ? U 3 : unknown;
用 T 匹配一个模式类型,提取 this 的类型到 infer 声明的局部变量 U 里返回。
OmitThisParameter
如果我们希望删除this的类型,可以使用OmitThisParameter

Partial
Partial 的源码是这样的:
js1type Partial<T> = { 2 [P in keyof T]?: T[P]; 3};
跟我们实现的差不多,本质上就是用 in遍历映射类型,值则使用索引访问,最后加上?就构造出一个全部可选的新类型了。
Required
源码跟 Partial类似,就是把所有?都删掉
js1type Required<T> = { 2 [P in keyof T]-?: T[P]; 3};
Readonly
源码类似同上:
js1type Readonly<T> = { 2 readonly [P in keyof T]: T[P]; 3};
Pick
使用映射类型的语法,在构造新的索引类型时,过滤出一些想要的属性:
js1type Pick<T, K extends keyof T> = { 2 [P in K]: T[P]; 3};
Record
TypeScript 内置了高级类型 Record 来创建索引类型,它的源码是这样的:
js1type Record<K extends string | number | symbol, T> = { [P in K]: T; }
它的源码刚好对应 JavaScript 的对象类型,JavaScript 的对象类型支持 string、number 和 symbol 作为键。
指定索引和值的类型分别为 K 和 T,就可以创建一个对应的索引类型。
当我们想要创建一个键为任意string的,值为对象类型的索引类型时,很多时候会这么创建:
js1type Obj = { 2 [key: string]: object, 3}
现在我们也可以这么创建:
js1type Obj = Record<string, object>
同时,也可以更好地限制 key和 value 的类型
js1interface CatInfo { 2 age: number; 3 breed: string; 4} 5 6type CatName = 'miffy' | 'boris' | 'mordred' 7 8const cats: Record<CatName, CatInfo> = { 9 miffy: { age: 10, breed: 'Persian' }, 10 boris: { age: 5, breed: 'Maine Coon' }, 11 mordred: { age: 16, breed: 'British Shorthair' }, 12}
Exclude
当我们想要从一个联合类型中去掉一部分类型时,可以使用Exclude类型:

源码:
js1type Exclude<T, U> = T extends U ? never : T
解释一下:如果传入的 T 与 U 能够匹配上过滤掉 U 的类型,剩下的类型组成联合类型。也就是取差集。
Extract
跟 Exclude 的作用相反,Extract的作用是取交集。

源码:
js1type Extract<T, U> = T extends U ? T : never
Omit
Pick可以提取出索引类型的一部分来构造出新的索引类型。
Omit是相反的,它会将在构造新的索引类型时删除掉一部分。
js1type Omit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>;

Awaited
Awaited关键字可以返回 Promise 的返回值
js1const func = Promise.resolve().then(() => 123) 2type FuncReturnType = Awaited<typeof func> // number
NonNullable
NonNullable 就是用于判断是否为非空类型,也就是不是 null 或者 undefined 的类型的,实现比较简单:
js1type NonNullable<T> = T extends null | undefined ? never : T;
当传入的是 null 或者 undefined 时会返回 never 类型,否则就返回其他类型:

Uppercase
字符串大写
Lowercase
字符串小写
Capitalize
首字母大写
Uncapitalize
去掉首字母大写



