整理自林不渡掘金小册
字面量类型和枚举
字面量
interface Tmp {
bool: true | false;
num: 1 | 2 | 3;
str: "lin" | "bu" | "du"
}
联合类型
一组类型的可用集合
interface Tmp {
mixed: true | string | 599 | {} | (() => {}) | (1 | 2)
}
对象字面量类型
interface Tmp {
obj: {
name: "linbudu",
age: 18
}
}
const tmp: Tmp = {
obj: {
name: "linbudu",
age: 18
}
}
枚举
类似于constants中的
enum PageUrl {
Home_Page_Url = "url1",
Setting_Page_Url = "url2",
Share_Page_Url = "url3",
}
const home = PageUrl.Home_Page_Url;
如果没有声明枚举值,默认使用数字枚举,从 0 开始
enum Items {
Foo,
Bar,
Baz
}
枚举和对象的区别在于,前者是双向映射,后者是单向的,但是双向映射只存在只有值为数字的枚举成员
enum Items {
Foo,
Bar,
Baz
}
const fooValue = Items.Foo; // 0
const fooKey = Items[0]; // "Foo"
常量枚举
和普通枚举的区别在于,常量枚举不支持从值访问成员
const enum Items {
Foo,
Bar,
Baz
}
const fooValue = Items.Foo; // 0
函数
函数的类型签名
type FuncFoo = (name: string) => number
const foo: FuncFoo = (name) => {
return name.length
}
描述函数的类型结构
interface FuncFooStruct {
(name: string): number
}
可选参数和rest参数
function foo1(name: string, age?: number): number {
const inputAge = age || 18; // 或使用 age ?? 18
return name.length + inputAge
}
或者是设置一个默认值
function foo(name: string, age: number = 18): number {
const inputAge = age || 18;
return name.length + inputAge
}
对于 rest 参数类型标注,本质上是一个数组
function foo(arg1: string, ...rest: any[]) { }
也可以使用元组类型
function foo(arg1: string, ...rest: [number, boolean]) { }
foo("linbudu", 18, true)
异步函数
async function asyncFunc(): Promise<void> {}
Class
类与类成员的类型签名
类的主要结构有主要结构只有构造函数、属性、方法和访问符(Accessor)
class Foo {
prop: string;
constructor(inputProp: string) {
this.prop = inputProp;
}
print(addon: string): void {
console.log(`${this.prop} and ${addon}`)
}
get propA(): string {
return `${this.prop}+A`;
}
set propA(value: string) {
this.prop = `${value}+A`
}
}
修师符
主要有 public / private / protected / readonly,用于修饰成员
- public:此类成员在类、类的实例、子类中都能被访问。
- private:此类成员仅能在类的内部被访问。
- protected:此类成员仅能在类与子类中被访问,你可以将类和类的实例当成两种概念,即一旦实例化完毕(出厂零件),那就和类(工厂)没关系了,即不允许再访问受保护的成员。
也可以在构造函数中对参数应用访问修师傅,这样参数会直接作为类的成员,也就是实例的属性
class Foo {
constructor(public arg1: string, private arg2: boolean) { }
}
new Foo("linbudu", true)
静态成员
在类的内部静态成员无法通过 this 来访问,需要通过Foo.staticHandler的形式进行访问
class Foo {
static staticHandler() { }
public instanceHandler() { }
}
继承、实现、抽象类
class Base { }
class Derived extends Base { }
抽象类
抽象类描述了一个类当中应该有哪些成员(属性、方法等)
abstract class AbsFoo {
abstract absProp: string;
abstract get absGetter(): string;
abstract absMethod(name: string): string
}
实现抽象类
class Foo implements AbsFoo {
absProp: string = "linbudu"
get absGetter() {
return "linbudu"
}
absMethod(name: string) {
return name
}
}
私有构造函数
为什么需要一个不能实例化的类?实际上,在工具类的场景中,如果里面全是静态属性,我们就可以使用私有构造函数防止实例化
class Utils {
public static identifier = "linbudu";
private constructor(){}
public static makeUHappy() {
}
}
Any、unknown、nerver
any 和 unknown
使用须知
- 如果是类型不兼容报错导致你使用 any,考虑用类型断言替代,我们下面就会开始介绍类型断言的作用。
- 如果是类型太复杂导致你不想全部声明而使用 any,考虑将这一处的类型去断言为你需要的最简类型。如你需要调用
foo.bar.baz(),就可以先将 foo 断言为一个具有 bar 方法的类型。 - 如果你是想表达一个未知类型,更合理的方式是使用 unknown。
any 和 unknown 的区别在于,any 支持所有的类型,而 unknown 是一定会得到一个确定的类型
never
never 代表是一个什么都没有的类型,甚至不包括空的乐喜,严格来说 nerver 类型不携带任何的类型信息
通常我们不会显式地声明一个 never 类型,它主要被类型检查所使用。但在某些情况下使用 never 确实是符合逻辑的,比如一个只负责抛出错误的函数
function justThrow(): never {
throw new Error()
}
在类型流的分析中,一旦一个返回值类型为 never 的函数被调用,那么下方的代码都会被视为无效的代码(即无法执行到):
function justThrow(): never {
throw new Error()
}
function foo (input:number){
if(input > 1){
justThrow();
// 等同于 return 语句后的代码,即 Dead Code
const name = "linbudu";
}
}
还可以用于类型检查
if (typeof strOrNumOrBool === "string") {
// 一定是字符串!
strOrNumOrBool.charAt(1);
} else if (typeof strOrNumOrBool === "number") {
strOrNumOrBool.toFixed();
} else if (typeof strOrNumOrBool === "boolean") {
strOrNumOrBool === true;
} else {
const _exhaustiveCheck: never = strOrNumOrBool;
throw new Error(`Unknown input type: ${_exhaustiveCheck}`);
}
类型断言
类型断言能够显式告知类型检查程序当前这个变量的类型,可以进行类型分析地修正、类型。它其实就是一个将变量的已有类型更改为新指定类型的操作,它的基本语法是 as NewType,你可以将 any / unknown 类型断言到一个具体的类型:
let unknownVar: unknown;
(unknownVar as { foo: () => {} }).foo();
interface IFoo {
name: string;
}
declare const obj: {
foo: IFoo
}
const {
foo = {} as IFoo
} = obj
非空断言
非空断言其实是类型断言的简化,它使用 ! 语法,即 obj!.func()!.prop 的形式标记前面的一个声明一定是非空的(实际上就是剔除了 null 和 undefined 类型),比如这个例子:
foo.func!().prop!.toFixed();
foo.func().prop.toFixed();
类型工具
类型别名
类型别名的作用主要是对一组类型或者一个特定类型结构进行封装
抽离一组联合类型:
type StatusCode = 200 | 301 | 400 | 500 | 502;
type PossibleDataTypes = string | number | (() => unknown);
const status: StatusCode = 502;
抽离一个函数类型:
type Handler = (e: Event) => void;
const clickHandler: Handler = (e) => { };
const moveHandler: Handler = (e) => { };
const dragHandler: Handler = (e) => { };
在类型别名中,类型别名可以这么声明自己能够接受泛型(我称之为泛型坑位)。一旦接受了泛型,就叫它工具类型:
type Factory<T> = T | number | string;
type FactoryWithBool = Factory<boolean>;
const foo: FactoryWithBool = true;
这样就可以实现更灵活的类型创建功能,通过这个去声明一个简单、有实际意义的工具类型
type MaybeNull<T> = T | null;
type MaybeNull<T> = T | null;
function process(input: MaybeNull<{ handler: () => {} }>) {
input?.handler();
}
类似的还有MaybePromise、MaybeArray
联合类型和交叉类型
联合类型的符号是|代表或,而交叉类型的符号是&,代表需要符合这里的所有类型,对于重复的类型会进行合并
interface NameStruct {
name: string;
}
interface AgeStruct {
age: number;
}
type ProfileStruct = NameStruct & AgeStruct;
const profile: ProfileStruct = {
name: "linbudu",
age: 18
}
索引签名类型
索引签名类型主要指的是在接口或类型别名中,通过以下语法来快速声明一个键值类型一致的类型结构:
interface AllStringTypes {
[key: string]: string;
}
type AllStringTypes = {
[key: string]: string;
}
对于以上的代码,代表我们只可以声明字符串类型的键,而键值实际上可以是数字类型也可以是 symbol
同时也可以存在其他的键值
interface AllStringTypes {
// 类型“number”的属性“propA”不能赋给“string”索引类型“boolean”。
propA: number;
[key: string]: boolean;
}
索引类型查询
keyof 操作符可以把对象中所有的键转为对应的字面量类型,然后组合成一个联合类型
interface Foo {
linbudu: 1,
599: 2
}
type FooKeys = keyof Foo; // "linbudu" | 599
索引类型访问
可以通过obj[expression]去访问一个对象属性,这里的 expression 是一个类型
interface NumberRecord {
[key: string]: number;
}
type PropType = NumberRecord[string]; // number
通过字面量类型进行索引的访问
interface Foo {
propA: number;
propB: boolean;
}
type PropAType = Foo['propA']; // number
type PropBType = Foo['propB']; // boolean
加入 keyof 就可以快速遍历所有的类型。并组合成一个联合类型
interface Foo {
propA: number;
propB: boolean;
propC: string;
}
type PropTypeUnion = Foo[keyof Foo]; // string | number | boolean
注意:如果没有索引签名是无法通过字面量类型进行访问的
映射类型
type Stringify<T> = {
[K in keyof T]: string;
};
这里会接受一个对象类型,使用 keyof 去获取这个对象类型的键名组成字面量的联合类型,然后通过映射类型(in)去把每一个成员映射出来
举例:
[K in keyof T]: string:代表所有的类型都是 stringT:接收一个对象[keyof T]:获取一个联合类型string | number | boolean | ()=>void[K in keyof T]:把每个键映射出来,K代表每一个键prop1,prop2...StringifiedFoo的每个键值都是 string
interface Foo {
prop1: string;
prop2: number;
prop3: boolean;
prop4: () => void;
}
type StringifiedFoo = Stringify<Foo>;
// 等价于
interface StringifiedFoo {
prop1: string;
prop2: string;
prop3: string;
prop4: string;
}
类型查询操作符
const str = "linbudu";
const obj = { name: "linbudu" };
const nullVar = null;
const undefinedVar = undefined;
const func = (input: string) => {
return input.length > 10;
}
type Str = typeof str; // "linbudu"
type Obj = typeof obj; // { name: string; }
type Null = typeof nullVar; // null
type Undefined = typeof undefined; // undefined
type Func = typeof func; // (input: string) => boolean
类型守卫
function isString(input: unknown): input is string {
return typeof input === "string";
}
function foo(input: string | number) {
if (isString(input)) {
// 正确了
(input).replace("linbudu", "linbudu599")
}
if (typeof input === 'number') { }
// ...
}
isString 函数称为类型守卫,在它的返回值中,我们不再使用 boolean 作为类型标注,而是使用 input is string 这么个奇怪的搭配,拆开来看它是这样的:
- input 函数的某个参数;
is string,即 is 关键字 + 预期类型,即如果这个函数成功返回为 true,那么 is 关键字前这个入参的类型,就会被这个类型守卫调用方后续的类型控制流分析收集到。
类型断言守卫
let name: any = 'linbudu';
function assertIsNumber(val: any): asserts val is number {
if (typeof val !== 'number') {
throw new Error('Not a number!');
}
}
assertIsNumber(name);
// number 类型!
name.toFixed();
泛型
这个类型别名本质是一个函数 T 就是变量,返回值是一个包含 T 的联合类型
type Factory<T> = T | number | string;
条件类型
type IsEqual<T> = T extends true ? 1 : 2;
type A = IsEqual<true>; // 1
type B = IsEqual<false>; // 2
type C = IsEqual<'linbudu'>; // 2
泛型约束与默认值
像函数可以声明一个参数的默认值一样,泛型也有默认值
type Factory<T = boolean> = T | number | string;
const foo: Factory = false;
在泛型中,我们可以使用 extends 关键字来约束传入的泛型参数必须符合要求。关于 extends,A extends B 意味着 A 是 B 的子类型,
type ResStatus<ResCode extends number> = ResCode extends 10000 | 10001 | 10002
? 'success'
: 'failure';
type Res1 = ResStatus<10000>; // "success"
type Res2 = ResStatus<20000>; // "failure"
type Res3 = ResStatus<'10000'>; // 类型“string”不满足约束“number”。
当然这个泛型也可以使用默认值
type ResStatus<ResCode extends number = 10000> = ResCode extends 10000 | 10001 | 10002
? 'success'
: 'failure';
type Res4 = ResStatus; // "success"
对象类型中的泛型
这个接口描述了一个通用的响应类型结构,预留出了实际响应数据的坑位,在请求函数中就可以传入特定的响应类型
interface IRes<TData = unknown> {
code: number;
error?: string;
data: TData;
}
interface IUserProfileRes {
name: string;
homepage: string;
avatar: string;
}
function fetchUserProfile(): Promise<IRes<IUserProfileRes>> {}
type StatusSucceed = boolean;
function handleOperation(): Promise<IRes<StatusSucceed>> {}
泛型嵌套
interface IPaginationRes<TItem = unknown> {
data: TItem[];
page: number;
totalCount: number;
hasNextPage: boolean;
}
function fetchUserProfileList(): Promise<IRes<IPaginationRes<IUserProfileRes>>>
函数中的泛型
这个函数泛型声明了一个参数 T,当传入 T 的时候 T 会自动地填充为这个参数的类型,返回值的类型
function handle<T>(input: T): T {}
const author = "linbudu"; // 使用 const 声明,被推导为 "linbudu"
let authorAge = 18; // 使用 let 声明,被推导为 number
handle(author); // 填充为字面量类型 "linbudu"
handle(authorAge); // 填充为基础类型 number
同样的,也可以传入多个
function swap<T, U>([start, end]: [T, U]): [U, T] {
return [end, start];
}
const swapped1 = swap(["linbudu", 599]);
const swapped2 = swap([null, 599]);
const swapped3 = swap([{ name: "linbudu" }, {}]);
在函数中的泛型也可以对其进行约束
function handle<T extends string | number>(input: T): T {}
箭头函数的泛型
在编辑器中推荐使用第二种
const handle = <T>(input: T): T => {}
const handle = <T extends any>(input: T): T => {}
Class 中的泛型
class Queue<TElementType> {
private _list: TElementType[];
constructor(initial: TElementType[]) {
this._list = initial;
}
// 入队一个队列泛型子类型的元素
enqueue<TType extends TElementType>(ele: TType): TElementType[] {
this._list.push(ele);
return this._list;
}
// 入队一个任意类型元素(无需为队列泛型子类型)
enqueueWithUnknownType<TType>(element: TType): (TElementType | TType)[] {
return [...this._list, element];
}
// 出队
dequeue(): TElementType[] {
this._list.shift();
return this._list;
}
}
类型系统层级
条件类型基础
条件类型类似于三元表达式,extends含义是是否包含,这里的包含是根据类型系统层级去判断的
type LiteralType<T> = T extends string ? "string" : "other";
内置工具
工具类型的分类
- 对属性的修饰,包括对象属性和数组元素的可选/必选、只读/可写。我们将这一类统称为属性修饰工具类型。
- 对既有类型的裁剪、拼接、转换等,比如使用对一个对象类型裁剪得到一个新的对象类型,将联合类型结构转换到交叉类型结构。我们将这一类统称为结构工具类型。
- 对集合(即联合类型)的处理,即交集、并集、差集、补集。我们将这一类统称为集合工具类型。
- 基于 infer 的模式匹配,即对一个既有类型特定位置类型的提取,比如提取函数类型签名中的返回值类型。我们将其统称为模式匹配工具类型。
- 模板字符串专属的工具类型,比如神奇地将一个对象类型中的所有属性名转换为大驼峰的形式。这一类当然就统称为模板字符串工具类型了。
属性修饰工具类型
这一部分的工具类型主要使用属性修饰、映射类型与索引类型
type Partial<T> = {
[P in keyof T]?: T[P];
};
type Required<T> = {
[P in keyof T]-?: T[P];
};
type Readonly<T> = {
readonly [P in keyof T]: T[P];
};
其中,Partial 与 Required 可以认为是一对工具类型,它们的功能是相反的,而在实现上,它们的唯一差异是在索引类型签名处的可选修饰符,Partial 是 ?,即标记属性为可选,而 Required 则是 -?,相当于在原本属性上如果有 ? 这个标记,则移除它
结构工具类型
这一部分的工具类型主要使用条件类型、映射类型与索引类型,结构工具类型又分为结构声明和结构处理
结构声明
type Record<K extends keyof any, T> = {
[P in K]: T;
};
其中,K extends keyof any 即为键的类型,这里使用 extends keyof any 表明,传入的 K 可以是单个类型,也可以是联合类型,而 T 即为属性的类型
如下的前两个通常可以来代替object
// 键名均为字符串,键值类型未知
type Record1 = Record<string, unknown>;
// 键名均为字符串,键值类型任意
type Record2 = Record<string, any>;
// 键名为字符串或数字,键值类型任意
type Record3 = Record<string | number, any>;
结构处理
- Pick:接受两个泛型参数,T 是会进行结构处理的原类型(一般是对象类型),而 K 则被约束为 T 类型的键名联合类型
type Pick<T, K extends keyof T> = {
[P in K]: T[P];
};
type Omit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>;
具体的过程如下
interface Foo {
name: string;
age: number;
job: JobUnionType;
}
type PickedFoo = Pick<Foo, "name" | "age">
- Omit:和 Pick 是反向的,Pick 是保留传入的键,而 Omit 是移除这些键