💪别再迷茫!一份让你彻底掌控 TypeScript 类型系统的终极指南

TypeScript现在的普及度已经很高了,虽然它是一种静态类型定义,但它的庞大已足够我们要向对待一门语言一样对待它了,去深入学习,以便更好的利用它的能力。

当然了,我们首先要明确为什么需要它,而不是把它当作一种负担:

  • 编译时类型检查,避免了运行时才检查的数据类型错误,导致系统奔溃。
  • 提升开发效率,IDE基于类型系统提供了精准的代码补全、接口提示。减少了查询文档、查询API的成本。
  • 增强了代码可读性,通过类型注解帮助成员快速理解函数输入、输出,降低了协作成本。

类型系统基石

像一门语言有基本的语法一样,TypeScript也有基本的类型定义,这些基本类型对应JavaScript中的基本数据类型,包括numberstringbooleannullundefinedsymbolbigintobject

ts 复制代码
const name: string = "hboot";
const age: number = 18;
const message: string = `hello, ${name}`
const uniqueKey: symbol = Symbol("key");

空类型、任意类型、未知类型

void表示空类型,any表示任意类型,unknown表示未知类型。

  • void 常用于函数无返回值时的返回类型声明;
  • any 表示任意类型,失去了类型检查的能力,非必要不要用;
  • unknown 表示未知类型,可以用来代替any,但是它不能直接用,必须通过类型断言或类型守卫明确具体类型才能操作。
  • never 表示没有任何类型,变量和函数返回不会存在任何实际值。它是所有类型的字类型,可以赋值给任意类型。
ts 复制代码
function noReturn(msg: string): void {
    console.log(msg);
}

let name: any = "admin";
// 可以任意赋值
name = 123;
// 甚至是当作函数调用,这导致开发不易发现,运行时才报错
name():

let age: unknown = 18;
// 类型为未知,无法直接操作
age += 10;
// 通过类型守卫,明确类型为 number
if(typeof age === 'number') { 
    age += 10;
}

复合类型

复合类型就是基础类型的组合,包括数组、对象、元组、枚举、类。

数组

数组的类型定义T[]Array<T>

ts 复制代码
// 数组
let names: string[] = ['admin', 'user'];
let names: Array<string> = ['admin', 'user'];

对象

对象类型定义对象的属性名、属性类型。属性支持可选、只读、索引签名。

ts 复制代码
// 对象
let user: { name: string; age: number } = {
    name: 'hboot',
    age: 18
}

元组

元组是明确了数组长度以及元素类型的。

ts 复制代码
// 元组
let userInfo: [string, number] = ['hboot', 18];

枚举

枚举用于定义一组有特殊含义的常量,比如状态码、类型标识等。

ts 复制代码
// 枚举
const enum Status { 
    Success = 200,
    Fail = 500
}

未赋值时,枚举值从 0 开始递增。如果自定义了开始值,则后面的值递增。

ts 复制代码
const enum Status { 
  Success, // 0
  Fail // 1
}

const enum Status { 
  Success = 1, // 1
  Fail // 2
}

可以通过const enum修复枚举定义,在编译时仅保留常量值,减少代码体积。

Class

class是面向对象编程的核心。es6中已经增强了对类的支持,可以通过类定义对象,明确属性、方法。类不仅可以用来创建实例,也可以作为类型注解描述实例类型。

ts 复制代码
class Animal {
  name: string;
  constructor(name: string) {
    this.name = name;
  }
}

类的定义包括实例属性、实例方法、静态属性、静态方法,同时还支持通过访问修饰符控制成员可见。

子类可以通过extends继承父类的非private成员,重写父类方法。仅能继承一个父类。

ts 复制代码
class Dog extends Animal {
  WangWang() {
    return "WangWang";
  }
}

类可以通过implements实现接口,用于约束类的实现,包括属性、方法,仅约束结构,不提供实现。可实现多个接口

ts 复制代码
interface Animal {
  name: string;
  speak(): void;
}

class Dog implements Animal {
  name: string;
  speak(): void {
    console.log("WangWang");
  }
  constructor(name: string) {
    this.name = name;
  }
}

通过abstract关键字可以声明抽象类,抽象类不能被实例化。它只定义了字段、方法结构,未实现具体逻辑,它仅可被子类继承,并需要实现它定义的所有抽象成员。

高级类型

基础类型可以满足绝大多数业务,但面对复杂业务需要类型复用、条件判断、属性筛选则需要高级类型。

复用类型

为了复用类型,可以通过type \ interface定义类型。type可以定义任意类型,包括基础类型、复合类型、联合类型等,比较灵活;interface 只能定义对象类型,但是可以继承同名合并

ts 复制代码
// type
type Age = number;
type User = { 
    name: string;
    age: Age;
}
type Status = 'success' | 200;

// interface
interface User { 
    name: string;
    age: number;
}

interface SuperUser extends User { 
    role: string;
}

interface 不同于类class,它可以继承多个接口。

联合/交叉类型

联合类型通过|表示变量可以是任意其中一种类型;交叉类型通过&表示变量必须同时满足多个类型。

联合类型使用时需要类型守卫明确类型后才能操作,如果变量已经赋值,则会自动推导出类型;

ts 复制代码
type Age = number | string;

let age: Age = 18;
age+=2;

function agePlus(age: Age){ 
    if(typeof age === 'number'){ 
        age+=2;
    }
    return age;
}

交叉类型常用合并对象类型,需要满足所有类型条件。如果无法满足所有类型条件,则该类型为never

ts 复制代码
type User = { 
    name: string;
}
type Address = { 
    address: string;
}

type UserAddress = User & Address;

泛型

泛型是将类型参数化,可以不指定具体类型来定义函数、类、接口。在使用时在传入具体类型,它可以高度抽象定义类型,保障类型复用和类型安全。

ts 复制代码
// 函数泛型
function getData<T>(data: T): T {
    return data;
}

// 接口泛型
interface IData<T> {
    data: T;
}

// 类泛型
class Data<T> {
    data: T[] = [];
}

因为不知道具体类型,就无法获知这个类型有什么属性,可以调用什么方法。为了使用某个属性或者某个方法,我们使用泛型约束来指定这个类型必须有哪些属性或者方法。

ts 复制代码
function getData<T extends { name: string }>(data: T): string {
    return data.name;
}

在调用具有泛型的类型时,需要传递具体类型,有时候这个泛型我们知道在很多情况下就是某一个类型,避免重复传入,我们可以指定泛型的默认类型,从而在使用时不再需要传入。

ts 复制代码
type User = { 
    name: string;
}

function getData<T extends User = User>(data: T): string {
  return data.name;
}

泛型约束和默认类型不需要同时出现,也可以定义其一,根据需求使用。

类型运算

除了定义具体类型,还可以通过类型运算从一个类型得到一个新的类型。也可以称之为推导,从一个类型推导为另一个类型。类型运算也是导致一些复杂类型产生的因素。

类型守卫

类型守卫是在运行时判断数据类型,它可以精准到具体类型,用于类型收窄,包括unknown、联合类型、类继承。关键字包括typeof instanceof in is

typeof 用于判断基础类型。但是无法判断 nulltypeof null 返回 'object',可通过value!==null 进行判断。

ts 复制代码
function agePlus(age: string | number) {
  if (typeof age === "number") {
    age += 2;
  }
  return age;
}

instanceof 判断是否为某个类实例。

ts 复制代码
class Animal {
  name: string;
  constructor(name: string) {
    this.name = name;
  }
}

class Dog extends Animal {
  WangWang() {
    return "WangWang";
  }
}

class Cat extends Animal {
  MiaoMiao() {
    return "MiaoMiao";
  }
}

function speak(animal: Animal) {
  if (animal instanceof Dog) {
    return `${animal.name}:${animal.WangWang()}`;
  }
  if (animal instanceof Cat) {
    return `${animal.name}:${animal.MiaoMiao()}`;
  }

  return `${animal.name}`;
}

in 适用于对象类型定义,用于判断是否包含某个属性

ts 复制代码
type Dog = {
  a: string;
  b: number;
};
type Cat = {
  d: string;
  c: number;
};

function getAttr(animal: Dog | Cat) {
  if ("b" in animal) {
    return animal.a;
  }

  if ("d" in animal) {
    return animal.c;
  }

  return "unknown";
}

is 用于精准明确是什么类型,但是判断逻辑是我们自己写的,这就要求我们判断逻辑要精确,否则可能导致运行时错误。

ts 复制代码
type Dog = {
  a: string;
  b: number;
};
type Cat = {
  d: string;
  c: number;
};

// 内部逻辑自定义判断是否为某个类型
function isDog(animal: Dog | Cat): animal is Dog {
  return "b" in animal;
}

function getAttr(animal: Dog | Cat) {
  // 调用时如果为true,则推导为Dog
  if (isDog(animal)) {
    return animal.a;
  } else {
    return animal.c;
  }
}

类型断言

类型断言是将一个类型临时转为另一个类型,仅在编译阶段告诉TypeScript是什么类型,不能改变变量的实际类型。

类型断言需要开发者明确保证断言类型正确,否则可能会导致运行时异常。对于不确定的类型,因该优先使用类型守卫.

使用as断言类型,可以转unknown类型、父类型转子类型场景。

ts 复制代码
let age: unknown = 34;
// 可以断言类型为string,从而调用字符串的方法。
// 但是在运行时会报错
(age as string).toUpperCase();

还可以通过as const断言为只读常量,常用于固定值的类型约束。

ts 复制代码
// 类型为 string
const name = "hboot";

// 类型为字面量 "hboot" 类型
const name = "hboot" as const;

使用!断言非空,常用于访问某个可选属性时,断言非空

ts 复制代码
type Cat = {
  d?: string;
  c: number;
};

let cat: Cat = {
  c: 20,
};
// 未断言时调用报错属性 d 可能未定义
// cat.d.toUpperCase();

// 断言非空后则不会再提时报错,但是运行时报错 
cat.d!.toUpperCase();

所以,断言要谨慎使用、避免滥用,确保断言的类型是是实际数据类型的兼容类型。

还可以通过双层断言将一个明确的类型断言为另一个类型。

这个操作很危险,实例中将string类型断言为Cat对象类型去访问属性c,编译通过,运行时爆炸。

ts 复制代码
type Cat = {
  d?: string;
  c: number;
};

let name = "hboot";

(name as unknown as Cat).c;

运算符

运算符可以从一个类型的到另一个类型。类型安全,类型运算的操作有很多

keyof 得到对象类型的所有字段key类型构成的联合类型。

ts 复制代码
type Cat = {
  d?: string;
  c: number;
};
// 得到 Cat 的所有字段 key 类型 联合类型 "d" | "c"
type Keys = keyof Cat;

// 如果字段是索引签名,则返回索引签名的类型
type Cat = {
  [x: string]: unknown;
};
// 得到的是索引签名类型 string
type Keys = keyof Cat;

typeof 之前再类型守卫里已经介绍过了,它可以返回变量的类型(基本类型)。

索引访问(IndexedAccessType)获取类型,比如我们要获取对象类型里某个字段的类型,就可以使用索引获取。

ts 复制代码
type Cat = {
  d?: string;
  c: number;
};

// 获取字段类型为 string,但由于字段是可选 ?,
// 所以返回的是 string | undefined
type D = Cat["d"];

也可以通过联合类型获取到多个字段的类型,结果为一个联合类型。

ts 复制代码
type D = Cat["d"|"c"]

// 如果想要获取所有字段类型,可以通过keyof 获取到所有key的联合类型
type D = Cat[keyof Cat]

索引最重要的一点是可以对数组、元组元素的类型索引获取.

ts 复制代码
type names = [string, number];
// 索引第一个元素的类型
type Name = names[0];

type Names = Array<string>;
// 索引数组元素的类型
type Name = Names[number];

还可以搭配typeof 对变量的类型进行索引获取。

条件类型(ConditionalType),通过输入的类型,决定输出类型 ,通过extends关键字和三元表达式来标识。

ts 复制代码
type Name<T> = T extends string ? string : number;

// 输入泛型参数返回类型,
// 输出类型为 string
type Admin = Name<string>;
// 输出类型为 number
type User = Name<unknown>;

也可以声明一个函数书写更复杂的判断逻辑。既然通过extends关键字以及泛型参数,那么也可以增加泛型约束、泛型默认值。

映射类型(MappedType),从一个类型创建一个新类型,建立在索引签名语法之上,通过对对象的字段键值、字段类型进行转换操作。

ts 复制代码
type Cat = {
  name?: string;
  age: number;
};

type ToFunction<T> = {
  [K in keyof T]: () => T[K];
};

// 从基本类型转换成函数类型
type CatFunction = ToFunction<Cat>;
/**
 *  type CatFunction = {
 *    name?: (() => string | undefined) | undefined;
 *    age: () => number;
 *  }
 */

文本类型(LiteralType),通过扩展字符串生成文本类型,表现同字符串字面值相同。

ts 复制代码
type Name = "hboot";
type GoodName = `Good, ${Name}`;

利用文本类型扩展,可以结合映射类型,改变键值,得到一个全新的类型。

ts 复制代码
type ToFunction<T> = {
  [K in keyof T as `on_${string & K}`]: () => T[K];
};

// 就会得到不同键值、不同类型的新对象类型
type CatFunction = ToFunction<Cat>;
/**
 *  type CatFunction = {
 *    on_name?: (() => string | undefined) | undefined;
 *    on_age: () => number;
 *  }
 */

infer 类型推断,通常用于泛型参数推导;函数返回值类型推导

ts 复制代码
type Name<T> = T extends Array<infer U> ? U : T;

// 条件类型推导 得到数组元素类型 string
type str = Name<string[]>;
// 非数组类型,返回原类型 { name: string }
type info = Name<{ name: string }>;

内置工具函数

除了上述我们要手动去实现逻辑从而创建新的类型外,提供了内置函数可以直接使用,这些工具提供一些常用的类型转换逻辑。

Partial<T> 得到一个新类型,定义类型所有的字段都是可选的,与之相反的是Required<T>

ts 复制代码
type Cat = {
  name?: string;
  age: number;
};

type NewCat = Partial<Cat>;
/**
 * type NewCat = {
 *   name?: string;
 *   age?: number;
 * }
 */
type RequiredCat = Required<Cat>;
/**
 * type RequiredCat = {
 *   name: string;
 *   age: number;
 * }
 */

Pick<T,Keys> 从一个类型中选择某些字段得到一个新类型,Omit<T,Keys>从一个类型中删除某些字段得到一个新类型

ts 复制代码
type Cat = {
  name?: string;
  age: number;
};

type NewCat = Pick<Cat, 'name'>;
/**
 * type NewCat = {
 *   name?: string;
 * }
 */
type NewCat = Omit<Cat, 'age'>;
/**
 * type NewCat = {
 *   name?: string;
 * }
 */

Readonly<T> 得到一个新类型,定义类型所有的字段都是只读的,初始化后不可以修改

ts 复制代码
type Cat = {
  name?: string;
  age: number;
};

type ReadonlyCat = Readonly<Cat>;

let cat: ReadonlyCat = {
  name: "hboot",
  age: 18,
};

// 报错,不可以修改再赋值,属性是只读的
// cat.age = 20;

它没有与之相反定义的工具,怎么处理让所有只读属性都变为可编辑的呢?还记得上面讲过的映射类型吗?

在映射类型中通过使用-符号将所有字段的readonly修饰符都移除掉

ts 复制代码
type MutableType<T> = {
  -readonly [K in keyof T]: T[K];
};

let cat: MutableType<ReadonlyCat> = {
  name: "hboot",
  age: 18,
};

// 通过映射类型 -readonly 移除掉属性的只读修饰符
cat.age = 20;

Record<Keys,T> 可以创建一个由Keys组成的属性对应类型为T的对象类型

ts 复制代码
type Animal = Record<
  "dog" | "cat",
  {
    name: string;
    age: number;
  }
>;

/**
 * type Animal = {
 *   dog: {
 *     name: string;
 *     age: number;
 *   };
 *   cat: {
 *     name: string;
 *     age: number;
 *  }; 
 * }
 */

Exclude<U,T>从指定联合类型U排除某个满足 T类型的成员,得到新类型;与之相反的是Extract<U,T>

ts 复制代码
type Type = Exclude<"name" | "age" | 32, number | "name">;

/**
 * type Type = "age"
 */

type Type = Extract<"name" | "age" | 32, number | "name">;
/**
 * type Type = "name" | 32
 */

NonNullable<Type>排除nullundefined的成员,得到新类型

Parameters<Type> 从函数类型提取函数的参数类型,得到一个元组类型 ,非函数类型定义得到never类型。

ts 复制代码
type Fun = (name: string, age: number) => void;

type Params = Parameters<Fun>;
/**
 * type Params = [name: string, age: number]
 */

ConstructorParameters<Type> 从构造函数类型提取构造函数的参数类型,得到一个元组类型或数组类型

ts 复制代码
type FunConstructorParams = ConstructorParameters<FunctionConstructor>;
/**
 * type FunConstructorParams = string[]
 */

type ErrorConstructorParams = ConstructorParameters<ErrorConstructor>;
/**
 * type ErrorConstructorParams =  [message?: string, options?: ErrorOptions]
 */

ReturnType<Type> 从函数类型提取函数的返回值类型 ,对于非函数类型会得到any.

Awaited 用来获取异步函数的返回值类型

ts 复制代码
type Fun = () => Promise<{ data: string }>;

type FunType = Awaited<ReturnType<Fun>>;
/**
 * type FunType = {
 *   data: string;
 * }
 */

InstanceType<Type> 从构造函数类型提取构造函数的实例类型

ts 复制代码
type Fun = new () => { name: string };

type FunType = InstanceType<Fun>;
/**
 * type FunType = {
 *   name: string;
 * }
 */

class Animal {
  name: string;
  constructor(name: string) {
    this.name = name;
  }
}

type AnimalType = InstanceType<typeof Animal>;
/**
 * type AnimalType = Animal
 */

NoInfer<T>阻止从该处推导泛型参数类型

ts 复制代码
function compare<T extends string>(a: T[], b?: NoInfer<T>) {
  console.log(a, b);
}

// 锁定了泛型 T 类型是 "admin" | "test" , 所以传入参数 "hboot" 会提示报错
// 如果没有 NoInfer 则不会报错,推断类型 string
compare(["admin", "test"], "hboot");

ThisParameterType<T> 提取函数类型中的this参数的类型 ,如果没有this参数则返回unknown

OmitThisParameter<Type> 移除函数类型中的this参数,得到一个新的函数类型 ,如果没有this参数或不是函数类型则返回类型本身。

ThisType<Type> 用于指定方法上下文中的this类型,不会返回新类型 ,指定后在方法中就可以通过this访问到指定属性。但是需要开启noImplicitThis(tsconfig.json)标志才可以使用。

ts 复制代码
interface Animal {
  name: string;
}

type Dog = {
  speak(): void;
} & ThisType<Animal>;

const dog: Dog = {
  speak() {
    // 通过this 可以访问到 name 属性
    console.log(this.name);
  },
};

文本类型工具,用于处理文本,包括:

  • Uppercase<StringType> 将字符串转换为大写
  • Lowercase<StringType> 将字符串转换为小写
  • Capitalize<StringType> 将字符串的第一个字符转换为大写
  • Uncapitalize<StringType> 将字符串的开头字符转换为小写

命名空间

通过关键字namespace 定义空间名,用于分组相关的类型和值,避免命名冲突。并可通过export导出供外部使用

ts 复制代码
namespace Hboot {
  //外部无法访问
  const name = "hboot" as const;

  // 外部可以访问
  export function getName() {
    return name;
  }
}

Hboot.getName();
相关推荐
GISer_Jing2 小时前
深入拆解Taro框架多端适配原理
前端·javascript·taro
毕设源码-邱学长2 小时前
【开题答辩全过程】以 基于VUE的藏品管理系统的设计与实现为例,包含答辩的问题和答案
前端·javascript·vue.js
用户28907942162713 小时前
Spec-Kit应用指南
前端
酸菜土狗4 小时前
🔥 手写 Vue 自定义指令:实现内容区拖拽调整大小(超实用)
前端
ohyeah4 小时前
深入理解 React Hooks:useState 与 useEffect 的核心原理与最佳实践
前端·react.js
Cache技术分享4 小时前
275. Java Stream API - flatMap 操作:展开一对多的关系,拉平你的流!
前端·后端
apollo_qwe4 小时前
前端缓存深度解析:从基础到进阶的实现方式与实践指南
前端
周星星日记4 小时前
vue中hash模式和history模式的区别
前端·面试
Light604 小时前
Vue 高阶优化术:v-bind 与 v-on 的实战妙用与思维跃迁
前端·低代码·vue3·v-bind·组件封装·v-on·ai辅助开发