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

相关推荐
@@@@@@@@5413 小时前
JavaScript 的缺点与 TypeScript 的弥补
开发语言·javascript·typescript
用户09340777351415 小时前
HarmonyOS WPS Open SDK 实践:最小打开闭环与 enableEdit 分支处理
typescript·harmonyos
柯克七七1 天前
我把 bug 修得太干净了,测试组以为这个模块本来就没问题
前端·javascript·typescript
退休倒计时1 天前
【每日一题】LeetCode 39. 组合总和 TypeScript
算法·leetcode·typescript
yyt3630458411 天前
KlineChartQuant Tooltip 高频交互 200FPS 性能优化完整复盘
前端·经验分享·性能优化·typescript·vue·交互·chrome devtools
用户0934077735142 天前
HarmonyOS WPS Open SDK 实践:办公审批附件「打开—编辑—回传」代码实现
typescript·harmonyos
Flynt2 天前
TypeScript 5.8 让我少踩了一个大坑,顺手再聊聊 --erasableSyntaxOnly
前端·javascript·typescript
退休倒计时2 天前
【每日一题】LeetCode 17. 电话号码的字母组合 TypeScript
算法·leetcode·typescript
无情的西瓜皮4 天前
MCP 协议实战:从零搭建一个 MCP 服务器(Node.js 版)
vue.js·人工智能·typescript·node.js
退休倒计时4 天前
【每日一题】LeetCode 78. 子集 TypeScript
算法·leetcode·typescript