TypeScript 更安全的 6 个写法:别再用 any 了

摘要:TypeScript 很强大,但大多数开发者只用到了皮毛。遇到报错就甩一个 any,用 as 让编译器闭嘴,然后纳闷为什么运行时还是出错。下面这 6 个写法,能让你的 TypeScript 代码更安全,在 bug 跑到用户面前之前就抓住它。

TypeScript 很强大,但大多数开发者只用到了皮毛。遇到报错就甩一个 any,用 as 让编译器闭嘴,然后纳闷为什么运行时还是出错。

下面这 6 个写法,能让你的 TypeScript 代码更安全,在 bug 跑到用户面前之前就抓住它。


1. 用 unknown 代替 any

把某个值标成 any,等于告诉 TypeScript:“别管我,什么都别检查。”这跟不用 TypeScript 有什么区别?

unknown 是 any 的安全版本。你可以把任何值赋给它,但 TypeScript 不允许你对它做任何操作,除非你先做类型判断。

any 的问题

let data: any = fetchSomething();
data.toFixed(2);      // 没报错,但 data 要是字符串呢?
data.toUpperCase();   // 没报错,但 data 要是数字呢?
console.log(data.foo.bar.baz); // 运行直接炸

用 unknown 改正

let data: unknown = fetchSomething();
// data.toFixed(2);  ← 报错:Object is of type 'unknown'

if (typeof data === "string") {
  console.log(data.toUpperCase()); // 安全
}

if (typeof data === "number") {
  console.log(data.toFixed(2));    // 安全
}

什么时候用 unknown:API 响应、用户输入、URL 参数、JSON.parse() 的结果、catch 里的错误对象。


2. 用 satisfies 代替 as

as 是类型断言,你告诉 TypeScript“相信我,我比你懂”。问题是 TypeScript 真信你,哪怕你是错的。

satisfies 是 TypeScript 4.9 引入的。它验证值是否符合某个类型,同时保留更精确的推断类型。

as 的问题

type Color = "red" | "green" | "blue";

type Theme = {
  primary: Color;
  secondary: Color;
  surface: string;
};

const broken = {
  primary: "red",
  secondary: "grn",   // 拼写错了,但没报错
  surface: "#fff"
} as Theme;

用 satisfies 改正

const safe = {
  primary: "red",
  secondary: "grn",   // 报错:'"grn"' is not assignable to type 'Color'
  surface: "#fff"
} satisfies Theme;

satisfies 还有个好处:保留字面量类型。上面例子中 theme.primary 的类型是 "red",不是 Color 联合类型,后续代码能得到更精确的自动补全。

什么时候可以用 as:操作 DOM 时,比如 document.getElementById('x') as HTMLInputElement。其他情况优先用 satisfies。


3. 用 is 写自定义类型守卫

typeof 和 instanceof 不够用的时候,可以用 is 自己写类型判断函数。

type Species = "cat" | "dog";

interface Pet {
  species: Species;
  name: string;
}

class Cat implements Pet {
  species: Species = "cat";
  name: string;
  constructor(name: string) {
    this.name = name;
  }
  purr(): void {
    console.log(`${this.name} purrs softly...`);
  }
}

class Dog implements Pet {
  species: Species = "dog";
  name: string;
  constructor(name: string) {
    this.name = name;
  }
  fetch(): void {
    console.log(`${this.name} fetches the ball!`);
  }
}

function isCat(pet: Pet): pet is Cat {
  return pet.species === "cat";
}

function isDog(pet: Pet): pet is Dog {
  return pet.species === "dog";
}

function interact(pet: Pet) {
  if (isCat(pet)) {
    pet.purr();   // TypeScript 知道是 Cat
  } else if (isDog(pet)) {
    pet.fetch();  // TypeScript 知道是 Dog
  }
}

实际应用:校验 API 响应、处理可辨识联合类型、区分不同的错误类型。

小技巧:类型守卫和数组的 filter 配合很好用:const cats = pets.filter(isCat),TypeScript 会自动推断出 Cat[] 类型。


4. 用联合类型代替 Enum

TypeScript 的 enum 看起来方便,但有代价:会生成额外的 JavaScript 代码,数值枚举容易出问题,对 tree-shaking 不友好。

enum 的问题

enum Status {
  Pending,    // 0
  Active,     // 1
  Suspended,  // 2
}

const s: Status = 99;  // 没报错,但 99 根本不是有效的 Status

用联合类型改正

type Status = "pending" | "active" | "suspended";

function setStatus(status: Status) {
  console.log(`Setting status: ${status}`);
}

setStatus("active");    // 正常
setStatus("invalid");   // 报错

如果需要运行时对象来遍历或反向查找,可以用 as const:

const STATUS = {
  Pending: "pending",
  Active: "active",
  Suspended: "suspended",
} as const;

type Status = (typeof STATUS)[keyof typeof STATUS];
// type Status = "pending" | "active" | "suspended"

Object.values(STATUS).forEach(s => console.log(s));


5. 用 Record 标注对象类型

别用 Object 或 {} 做类型。Object 什么都能接,{} 表示“任何非空值”,都不是你想要的。

// 错误示范
const config: Object = new Date();  // 没报错
const what: {} = "hello";           // 没报错

// 正确写法
type Config = Record<string, unknown>;

const config: Config = {
  apiUrl: "https://api.example.com",
  timeout: 5000,
  debug: true,
};

Record 在约束键的场景更好用:

type Role = "admin" | "editor" | "viewer";
type Permissions = Record<Role, string[]>;

const perms: Permissions = {
  admin: ["read", "write", "delete"],
  editor: ["read", "write"],
  viewer: ["read"],
  // 少一个键会报错
};


6. 用模板字面量类型组合字符串

TypeScript 的模板字面量类型可以动态构造字符串类型。把两个联合类型放一起,TypeScript 自动生成所有可能的组合。

type Size = "sm" | "md" | "lg";
type Color = "primary" | "secondary" | "danger";

type ButtonVariant = `${Size}-${Color}`;
// 结果是 "sm-primary" | "sm-secondary" | "sm-danger" | "md-primary" | ...

const btn: ButtonVariant = "md-primary";   // 正常
const bad: ButtonVariant = "xl-primary";   // 报错

实际应用

// CSS 工具类
type Spacing = 0 | 1 | 2 | 4 | 8;
type Direction = "t" | "r" | "b" | "l" | "x" | "y";
type MarginClass = `m${Direction}-${Spacing}`;

// 事件处理函数名
type DomEvent = "click" | "focus" | "blur";
type HandlerName = `on${Capitalize<DomEvent>}`;
// 结果是 "onClick" | "onFocus" | "onBlur"

TypeScript 提供了 Capitalize、Uncapitalize、Uppercase、Lowercase 这些工具类型,可以在模板字面量里用。


速查表

要做什么避免用推荐用
未知类型anyunknown
类型验证assatisfies
类型收窄手动判断is 类型守卫
常量集合enum联合类型或 as const
对象类型Object 或 {}Record<K, V>
字符串组合手动列举模板字面量类型

好的 TypeScript 代码不在于写更多类型,而在于写对的类型。每个模式做一件事:让编译器替你抓 bug,这样用户就不用撞上它们了。

本文内容仅供个人学习、研究或参考使用,不构成任何形式的决策建议、专业指导或法律依据。未经授权,禁止任何单位或个人以商业售卖、虚假宣传、侵权传播等非学习研究目的使用本文内容。如需分享或转载,请保留原文来源信息,不得篡改、删减内容或侵犯相关权益。感谢您的理解与支持!

链接: https://shenqiku.cn/article/FLY_13730