TypeScript 进阶必备!5 个实用工具类型,帮你写出更健壮的前端代码

TypeScript 作为 JavaScript 的超集,凭借强大的类型系统让代码更健壮、更易维护。在日常开发中,自定义工具类型往往能帮我们高效解决类型安全问题。本文就分享 5 个实用的 TypeScript 自定义工具类型,或许能帮你规避常见坑点,甚至启发你打造专属工具类型来应对业务难题。

1. 用Process优雅处理状态流转

开发中你是否写过这样的状态定义?

TypeScript 复制代码
type State = {
  loading: boolean;
  error: string | null;
  data: User | null;
};

const state: State = {
  loading: true,
  error: "",
  data: null,
};

这种写法的问题很明显:后续判断状态时需要叠加多个条件,代码冗余且易出错,比如:

TypeScript 复制代码
if (loading && !error && data) {} // 条件判断越来越复杂

解决方案:用"区分联合类型"优化

通过status字段作为"区分标志",明确状态的唯一可能性,避免多条件叠加:

TypeScript 复制代码
type State =
  | { status: "idle" } // 初始空闲状态
  | { status: "busy" } // 加载中状态
  | { status: "ok"; data: User } // 成功状态(带数据)
  | { status: "fail"; error: string }; // 失败状态(带错误信息)

const state: State = { status: "idle" };

这样判断状态时只需检查status,类型系统会自动推断当前状态下可访问的字段(比如status: "ok"时才能访问data):

TypeScript 复制代码
if (state.status === "ok") {
  console.log(state.data); // 此处访问data完全安全
}

进阶:自定义Process工具类型

重复定义上述状态结构会产生大量冗余代码。自定义Process<TData, TError>工具类型,可复用异步流程的状态逻辑:

TypeScript 复制代码
// utility-types.ts
/**
 * 描述异步流程的状态
 * 默认TData为void(无数据),TError为Error(默认错误类型)
 */
type Process<TData = void, TError = Error, TSkipIdle = false> =
  // 可选移除idle状态(TSkipIdle为true时不包含idle)
  | (TSkipIdle extends false ? { status: "idle" } : never)
  | { status: "busy" } // 加载中状态
  // TData为void时,不包含data字段
  | (TData extends void ? { status: "ok" } : { status: "ok"; data: TData })
  // TError为void时,不包含error字段
  | (TError extends void ? { status: "fail" } : { status: "fail"; error: TError });

使用示例

TypeScript 复制代码
// 1. 包含idle状态,成功时返回User类型数据
const state: Process<User> = { status: "idle" };
// 2. 成功状态(带User数据)
const successState: Process<User> = { status: "ok", data: { id: "1", name: "测试用户" } };
// 3. 失败状态(默认Error类型)
const errorState: Process<User> = { status: "fail", error: new Error("请求失败") };
// 4. 移除idle状态(比如组件挂载即加载,无需初始空闲态)
const stateWithoutIdle: Process<Comment, Error, true> = { status: "busy" };

实际业务场景示例(异步请求用户数据)

用Process优化前后的代码对比,能明显看到逻辑更简洁、状态更明确:

TypeScript 复制代码
// 优化后
type State = Process<User>;
let state: State = { status: "idle" };

const getUser = async (userId: string) => {
  state = { status: "busy" }; // 开始加载
  try {
    const user = await apiCallToUser(userId);
    state = { status: "ok", data: user }; // 成功:更新数据
  } catch (error) {
    state = { status: "fail", error: "请求出错" }; // 失败:更新错误信息
  }
};

// 优化前(冗余且易出错)
type State = {
  loading: boolean;
  error: string | null;
  data: User | null;
};
let state: State = { loading: true, error: "", data: null };

const getUser = async (userId: string) => {
  state = { loading: true, error: null, data: null }; // 需手动重置所有字段
  try {
    const user = await apiCallToUser(userId);
    state = { loading: false, error: null, data: user }; // 手动关闭loading
  } catch (error) {
    state = { loading: false, error: "出错了", data: null }; // 易遗漏字段更新
  }
};

优化后优势:

  • 状态组合从 8 种(loading×error×data)减少到 4 种,逻辑更清晰;
  • 避免"忘记关闭 loading"等低级错误,类型系统自动校验状态合法性。

2. 用Result统一处理 API 交互

使用fetch或axios时,若忘记写.catch()会导致未处理的 Promise 错误;部分 API 还会返回非标准的错误数据格式,增加处理复杂度。

解决方案:Result工具类型

用Result<TData>统一 API 返回格式,明确区分"取消""失败""成功"三种状态:

TypeScript 复制代码
type Result<TData> =
  | { status: "aborted" } // 请求被取消
  | { status: "fail"; error: unknown } // 请求失败(带错误信息)
  | { status: "ok"; data: TData }; // 请求成功(带返回数据)

使用示例(API 请求)

TypeScript 复制代码
// 调用API,返回Result<User>类型
const result = await service<User>("https://api.example.com/user");

// 处理不同状态
if (result.status === "aborted") {
  return; // 取消请求,不做后续处理
}

if (result.status === "fail") {
  alert(`请求失败:${result.error}`); // 处理错误
  return;
}

if (result.status === "ok") {
  alert(`请求成功:${result.data.name}`); // 处理成功数据
  return;
}

//  exhaustive check(完整性校验):确保所有状态都被处理
// 若遗漏某个状态,TypeScript会编译报错
const _exhaustiveCheck: never = result;

React 场景的额外优势

在 React 的useEffect中使用时,可轻松处理"请求取消"场景(比如组件卸载前取消请求),避免"已卸载组件触发 setState"的警告:

TypeScript 复制代码
useEffect(() => {
  const controller = new AbortController();

  const fetchUser = async () => {
    const result = await service<User>("https://api.example.com/user", {
      signal: controller.signal,
    });

    if (result.status === "aborted") return; // 组件卸载后,请求被取消,不更新状态
    if (result.status === "ok") setUser(result.data);
  };

  fetchUser();

  // 组件卸载时取消请求
  return () => controller.abort();
}, []);

3. 用Brand避免"原始类型滥用"

"原始类型滥用"是常见代码坏味道:用string/number等原始类型表示业务概念(如用户 ID、文档 ID),易导致类型混淆。比如:

TypeScript 复制代码
// 函数接收用户ID(string类型)
const getUserPosts = (userId: string) => {
  // 根据用户ID查询文章
};

const documentId = "doc-123"; // 文档ID(也是string类型)
getUserPosts(documentId); // TypeScript无报错,但逻辑上是错误的!

TypeScript 无法区分"用户 ID"和"文档 ID"(两者都是string),导致潜在 bug。

解决方案:自定义Brand工具类型

通过"品牌化类型"(Branded Type)为原始类型添加"唯一标识",让 TypeScript 区分不同业务含义的原始类型:

TypeScript 复制代码
// Brand工具类型:TData为原始类型,TLabel为业务标识
type Brand<TData, TLabel extends string> = TData & { __brand: TLabel };

// 定义"用户ID"类型(string + 品牌标识'UserId')
type UserId = Brand<string, "UserId">;
// 定义"文档ID"类型(string + 品牌标识'DocumentId')
type DocumentId = Brand<string, "DocumentId">;

使用示例

TypeScript 复制代码
// 函数参数改为UserId类型
const getUserPosts = (userId: UserId) => {
  // ...
};

const documentId = "doc-123"; // DocumentId类型(或普通string)
getUserPosts(documentId); // TypeScript编译报错!
// 错误信息:Argument of type 'string' is not assignable to parameter of type 'UserId'

// 正确用法:通过类型断言创建UserId(建议在验证函数中使用)
const validUserId = "user-456" as UserId;
getUserPosts(validUserId); // 无报错

关键特性

  • 无运行时开销:__brand字段仅在编译时存在,编译后会被移除,不影响代码性能;
  • 强制类型校验:必须显式创建"品牌化类型",避免无意识的类型混淆。

4. 用Prettify优化类型显示

使用Pick/Omit或交叉类型(&)创建复杂类型时,编辑器 hover 显示的类型定义往往冗长混乱(比如显示Pick<User, "id" | "name"> & { age: number }),而非简洁的扁平结构。

解决方案:Prettify工具类型

通过Prettify将复杂类型"扁平化",让编辑器显示更友好的类型定义:

TypeScript 复制代码
type Prettify<TObject> = {
  [Key in keyof TObject]: TObject[Key]; // 遍历对象所有属性
} & {}; // 强制TypeScript展开类型结构

使用示例

TypeScript 复制代码
// 复杂类型(Pick + 交叉类型)
type UserBase = Pick<User, "id" | "name">;
type UserWithAge = UserBase & { age: number };

// 未使用Prettify:hover显示 "Pick<User, 'id' | 'name'> & { age: number }"
type UglyUser = UserWithAge;

// 使用Prettify:hover显示 "{ id: string; name: string; age: number }"
type PrettyUser = Prettify<UserWithAge>;

使用建议

无需全局滥用,仅在"类型结构复杂、hover 显示混乱"时使用(比如公共组件的 Props 类型、复杂业务模型)。

5. 用StrictURL实现类型安全的路由

手动拼接 URL(如'/users/' + userId)易出错(比如少写斜杠、参数格式错误)。StrictURL工具类型可在编译时强制校验 URL 结构,避免运行时错误。

StrictURL实现代码

TypeScript 复制代码
// 递归拼接路径片段(无末尾斜杠)
type ToPath<TItems extends string[]> = TItems extends [
  infer Head extends string,
  ...infer Tail extends string[]
]
  ? `${Head}${Tail extends [] ? "" : `/${ToPath<Tail>}`}`
  : "";

// 递归构建查询参数字符串
type ToQueryString<TParams extends string[]> = TParams extends [
  infer Head extends string,
  ...infer Tail extends string[]
]
  ? `${Head}=${string}${Tail extends [] ? "" : `&${ToQueryString<Tail>}`}`
  : "";

// 主工具类型:构建完整URL类型
type StrictURL<
  TProtocol extends "https" | "http", // 协议(仅支持http/https)
  TDomain extends `${string}.${"com" | "dev" | "io"}`, // 域名(仅支持.com/.dev/.io)
  TPath extends string[] = [], // 路径片段(可选)
  TParams extends string[] = [] // 查询参数(可选)
> = `${TProtocol}://${TDomain}${TPath extends [] ? "" : `/${ToPath<TPath>}`}${TParams extends [] ? "" : `?${ToQueryString<TParams>}`}`;

核心原理拆解

  1. infer关键字:类型层面的"解构赋值",从数组类型中提取"首元素"(Head)和"剩余元素"(Tail);
  2. 递归条件类型:通过递归处理数组的每个元素,直到数组为空(基准条件),最终拼接成完整的 URL 字符串类型。

使用示例

TypeScript 复制代码
// 1. 带动态路径的路由(articles/[id]/detail)
type ArticleDetailRoute = StrictURL<"https", "example.com", ["articles", string, "detail"]>;
// hover显示:"https://example.com/articles/${string}/detail"

// 2. 带查询参数的路由(search?q=&source=)
type SearchRoute = StrictURL<"https", "google.com", ["search"], ["q", "source"]>;
// hover显示:"https://google.com/search?q=${string}&source=${string}"

// 3. 错误示例:域名后缀不合法(TypeScript编译报错)
type InvalidDomainRoute = StrictURL<"https", "example.cn", ["users"]>;
// 错误信息:Type '"example.cn"' does not satisfy the constraint `${string}.${"com" | "dev" | "io"}`

行业应用

现代框架(如 Next.js 13+的 App Router)已采用类似思路实现"类型安全路由",从编译阶段规避"URL 路径错误""参数缺失"等问题。

总结

本文介绍的 5 个工具类型,覆盖了"状态管理""API 交互""类型安全""类型显示优化""路由校验"等高频场景。它们的核心价值在于:

  1. 减少重复代码,提升开发效率;
  2. 利用 TypeScript 类型系统,提前规避运行时 bug;
  3. 让代码结构更清晰,降低团队协作成本。

TypeScript 的工具类型生态远不止于此,建议根据实际业务需求灵活扩展(比如为表单校验、权限控制创建专属工具类型)。AI 工具(如 ChatGPT、Cursor)可辅助快速生成复杂工具类型,但理解其底层原理(如递归、infer、交叉类型)仍是关键。

掌握这些工具类型,能让你的 TypeScript 代码更健壮、更易维护,也能更好地适应"类型驱动开发"的行业趋势。

扩展链接

SpreadJS------可嵌入您系统的在线Excel

相关推荐
烛阴2 小时前
TypeScript 函数重载入门:让你的函数签名更精确
前端·javascript·typescript
随笔记3 小时前
react中函数式组件和类组件有什么区别?新建的react项目用函数式组件还是类组件?
前端·react.js·typescript
定栓4 小时前
Typescript入门-对象讲解
前端·javascript·typescript
ssshooter15 小时前
VSCode 自带的 TS 版本可能跟项目TS 版本不一样
前端·面试·typescript
Lsx_1 天前
TypeScript 是怎么去查找类型定义的?
前端·javascript·typescript
知识分享小能手1 天前
Vue3 学习教程,从入门到精通,Axios 在 Vue 3 中的使用指南(37)
前端·javascript·vue.js·学习·typescript·vue·vue3
任磊abc2 天前
vscode无法检测到typescript环境解决办法
ide·vscode·typescript
烛阴2 天前
精简之道:TypeScript 参数属性 (Parameter Properties) 详解
前端·javascript·typescript
Data_Adventure2 天前
Java 与 TypeScript 的“同名方法”之争:重载机制大起底
前端·typescript