TypeScript:停止使用额外步骤编写 JavaScript

发布: (2026年2月17日 GMT+8 05:02)
11 分钟阅读
原文: Dev.to

I’m happy to translate the article for you, but I’ll need the full text of the post (the parts you want translated). Could you please paste the content you’d like me to convert into Simplified Chinese? Once I have it, I’ll keep the source link and formatting exactly as you requested.

Introduction

你已经第 47 次在本季度盯着 Cannot read property 'email' of undefineduser 对象本应有一个 email。API 文档说它会有一个 email。六个月前离职的同事明确说它会有一个 email。

但它没有。现在你正在调试一个代码库,里面 any 出现了 847 次,而且每个函数都接受“随便吧”作为有效参数。

欢迎来到 JavaScript 地狱。如果使用得当,TypeScript 就是出口——只要你用对了它。

TypeScript 是什么

TypeScript 是一种静态类型系统,能够在编译时捕获错误。它是带有护栏的 JavaScript,能够:

  • 告诉你 当你访问不存在的属性时
  • 自动完成 你的对象,因为它知道对象里有什么
  • 通过类型 为代码提供文档(不再需要 // user object, has stuff 注释)
  • 无畏重构,因为编译器会在用户发现之前先报错

把它想象成逻辑的拼写检查。发送前会出现红色波浪线。

TypeScript 不是

  • 运行时安全网 — 类型在编译后消失。TypeScript 无法在运行时拯救你免受错误的 API 响应。
  • 性能提升器 — 它编译成 JavaScript。速度相同,输出相同。
  • 过度工程的借口type NestedGenericFactoryBuilderStrategy, K> 并不是炫技。
  • 仅仅是“带类型的 JavaScript” — 正确使用时,它改变了你设计代码的方式,而不仅仅是标注代码的方式。

学习 TypeScript 所需的一切

  • Cheatsheets 用于语法相关的内容——类型、接口、类、控制流。
  • The handbook 帮助你入门。
  • A solid grasp of tsconfig.json 在动手之前先弄清楚。很多时候“TypeScript 不起作用”其实是因为“不明白 strict: true 的作用”。省去自己的焦虑吧。

实践(muscle memory)

挑选一个小项目——一个 CLI 工具、一个实用库、一个 API 客户端——并严格地为其添加类型。你会发现,一个 strict: true 项目带来的收获,胜过十个教程。

本文的其余部分?是思考的部分——如何从“TypeScript that compiles”进化到“TypeScript that protects”。

重构之旅:从 any 到类型安全

0 % — 问题

async function getUser(id: any): Promise {
  const res = await fetch(`/api/users/${id}`);
  return res.json();
}

const user = await getUser(123);
console.log(user.emial); // typo? TypeScript: LGTM 👍

问题: 完全没有类型安全。any 进,any 出。

25 % — 基础接口

interface User {
  id: number;
  name: string;
  email: string;
}

async function getUser(id: number): Promise {
  const res = await fetch(`/api/users/${id}`);
  return res.json(); // ⚠️ lying to TypeScript
}

const user = await getUser(123);
console.log(user.emial); // ✅ Error: Property 'emial' does not exist

作用: 捕获拼写错误。自动补全生效。你的 IDE 现在有用了。

问题:承诺 API 会返回 User,但 API 可能会说谎。没有运行时校验。

50 % — 可选属性与空值处理

interface User {
  id: number;
  name: string;
  email?: string;               // 可能不存在
  avatar?: string | null;      // 可能为 null
}

async function getUser(id: number): Promise {
  const res = await fetch(`/api/users/${id}`);
  if (!res.ok) return null;
  return res.json();
}

const user = await getUser(123);
if (user) {
  console.log(user.email?.toUpperCase()); // 安全链式调用
}

作用: 贴合现实——属性可能缺失或为 null。强制你处理边界情况。

问题: 仍然盲目信任 res.json()。没有校验返回的数据是否真的匹配 User

75 % — 用判别联合描述 API 状态

interface User {
  id: number;
  name: string;
  email?: string;
}

type ApiResult =
  | { status: "loading" }
  | { status: "error"; message: string }
  | { status: "success"; data: T };

async function getUser(id: number): Promise> {
  try {
    const res = await fetch(`/api/users/${id}`);
    if (!res.ok) {
      return { status: "error", message: `HTTP ${res.status}` };
    }
    const data = await res.json();
    return { status: "success", data };
  } catch {
    return { status: "error", message: "Network failed" };
  }
}

const result = await getUser(123);

switch (result.status) {
  case "loading":
    showSpinner();
    break;
  case "error":
    showError(result.message); // TS 知道这里一定有 `message`
    break;
  case "success":
    showUser(result.data);     // TS 知道这里的 `data` 是 User
    break;
}

作用: 描述所有可能的状态。TypeScript 在每个分支中收窄类型。status"error" 时不可能访问 data

问题: 仍然没有运行时保证 dataUser 匹配。如果 API 改动,只有在生产环境才会发现。

100 % — 使用类型守卫进行运行时校验

interface User {
  id: number;
  name: string;
  email?: string;
}

// 类型守卫:在运行时校验,在编译时收窄
function isUser(obj: unknown): obj is User {
  return (
    typeof obj === "object" &&
    obj !== null &&
    "id" in obj &&
    typeof (obj as User).id === "number" &&
    "name" in obj &&
    typeof (obj as User).name === "string"
  );
}

type ApiResult =
  | { status: "error"; message: string }
  | { status: "success"; data: T };

async function getUser(id: number): Promise> {
  const res = await fetch(`/api/users/${id}`);
  if (!res.ok) {
    return { status: "error", message: `HTTP ${res.status}` };
  }
  const data = await res.json();
  if (isUser(data)) {
    return { status: "success", data };
  }
  return { status: "error", message: "Invalid payload" };
}

作用: 在运行时确保 payload 符合 User。类型守卫在编译时收窄类型,让你同时获得安全性和自动补全。

结果: 你已经从 “any” 迁移到完整的、经过校验的类型流——代码在开发阶段快速报错,同时在生产环境也能提供保护。

代码示例

try {
  const res = await fetch(`/api/users/${id}`);
  if (!res.ok) {
    return { status: "error", message: `HTTP ${res.status}` };
  }
  const json: unknown = await res.json(); // don't trust it

  if (!isUser(json)) {
    return { status: "error", message: "Invalid user data" };
  }

  return { status: "success", data: json }; // TS knows it's User
} catch {
  return { status: "error", message: "Network failed" };
}

功能说明

  • 将 API 响应视为 unknown(诚实)
  • 在运行时使用类型守卫进行验证
  • 验证通过后,TypeScript 会将类型收窄为 User
  • 在合同变更影响用户之前捕获 API 变更

生产就绪。 对于复杂的模式,建议使用 ZodValibot 等库,而不是手写守卫。

当前格局

框架常用选择原因
Next.js / ReactZod, Valibot函数式风格,可摇树(tree‑shakable),无需装饰器
NestJSclass-validator + class-transformer基于装饰器,原生集成 NestJS 管道/DTO

接口 vs. 类型 vs. 类:决策树

Decision Tree

TL;DR

构造使用场景
Interface描述对象形状、公共 API、扩展
Type联合类型、交叉类型、元组、映射类型
Class运行时实例、私有状态、instanceof 检查

快速参考

问题🚩 红旗✅ 好处
“这是什么类型?”any 到处显式接口
“这可能为 null 吗?”未检查的 .property 访问可选链 + 空值检查
“这个函数可以返回什么?”Promise所有状态的联合类型
“这个 API 响应安全吗?”强制转换 as User运行时验证 + 类型守卫
“它有哪些属性?”console.log 来查找自动完成已知
“这次重构会破坏东西吗?”“部署并祈祷”合并前的编译错误

何时 使用(完整)TypeScript

  • 🚫 对所有内容进行过度类型标注 – 不要为只使用一次的对象创建接口。内联类型即可:

    function greet(user: { name: string }) {}
  • 🚫 为简单问题使用复杂泛型 – 如果你的类型定义比函数本身还长,请重新考虑。

  • 🚫 为你无法控制的第三方 API 响应进行类型标注 – 使用运行时校验,而不是用接口欺骗。

  • 🚫 将 100 % 类型覆盖率作为目标 – 在边界处使用 anyunknown 是可以接受的。完美是已发布代码的敌人。

TypeScript 光标规则与 AI 指南

创建一个 .cursorrules 文件(或在光标设置中添加规则):

# TypeScript Rules

- Never use `any` — use `unknown` and narrow with type guards
- Treat all external data as `unknown`, validate with Zod
- Use discriminated unions for state (loading/error/success)
- Prefer Result pattern over thrown exceptions
- `strict: true` always
  • 永不使用 any — 使用 unknown 并通过类型守卫进行缩小
  • 将所有外部数据视为 unknown,使用 Zod 进行验证
  • 对状态使用判别联合(loading / error / success)
  • 更倾向于使用 Result 模式而非抛出异常
  • 始终使用 strict: true

示例 AI 提示

Write a TypeScript function to fetch users from /api/users.

Requirements:

  • Use Zod for response validation
  • Return a discriminated union: { status: “error”; message: string } | { status: “success”; data: User[] }
  • Handle network errors and validation failures separately
  • Infer the User type from the Zod schema

结论

TypeScript 并不是到处随意添加冒号和尖括号。它的目标是 使非法状态不可表示

从这里开始:

  1. 启用 strict mode
  2. 诚实地建模你的 API 响应。
  3. 使用联合类型来表示状态。
  4. 在边界进行验证。

停止仅仅编写类型。开始用类型进行设计。

0 浏览
Back to Blog

相关文章

阅读更多 »