💪别再迷茫!一份让你彻底掌控 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();
相关推荐
崔庆才丨静觅9 小时前
hCaptcha 验证码图像识别 API 对接教程
前端
passerby60619 小时前
完成前端时间处理的另一块版图
前端·github·web components
掘了9 小时前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
崔庆才丨静觅9 小时前
实用免费的 Short URL 短链接 API 对接说明
前端
崔庆才丨静觅10 小时前
5分钟快速搭建 AI 平台并用它赚钱!
前端
崔庆才丨静觅10 小时前
比官方便宜一半以上!Midjourney API 申请及使用
前端
Moment10 小时前
富文本编辑器在 AI 时代为什么这么受欢迎
前端·javascript·后端
崔庆才丨静觅11 小时前
刷屏全网的“nano-banana”API接入指南!0.1元/张量产高清创意图,开发者必藏
前端
剪刀石头布啊11 小时前
jwt介绍
前端
爱敲代码的小鱼11 小时前
AJAX(异步交互的技术来实现从服务端中获取数据):
前端·javascript·ajax