欢迎关注微信公众号:FSA全栈行动 👋
写 Web 的时候,如果只是把 TypeScript 当成一个单纯的"报错检查器",那就太浪费了。在实际的工程实践中,TypeScript 能帮我们写出更安全、更简洁的代码,甚至能帮我们在编译阶段就拦住很多低级的逻辑错误。
我自己在写 Web 项目时,总结了这 10 个非常实用的技巧,希望能帮大家提升开发体验。
一、基础进阶:减少冗余与提升准确性
在处理基础类型定义时,很多小伙伴容易写出重复且不精确的代码。
1、用 satisfies 替代类型断言
以前在给对象指定类型时,我经常会习惯性地使用 as。但 as 实际上是一种"强行告诉编译器听我的"行为,它会掩盖掉一些潜在的配置错误。
现在我更倾向于使用 satisfies,它既能校验类型,又能保留对象最原始的字面量信息。
| 方式 | 校验能力 | 类型精度 | 风险 |
|---|---|---|---|
使用 as |
强行转换 | 丢失具体的字面量信息,降级为宽泛类型 | 容易掩盖错误的配置值 |
使用 satisfies |
严格校验 | 保留最精确的字面量类型 | 安全且准确 |
TypeScript
// 不推荐:使用 as
const config = {
theme: "dark",
language: "en",
} as AppConfig;
// 如果 AppConfig 要求 theme 必须是 "light",这里可能不会报错,但运行时逻辑会乱
// 推荐:使用 satisfies
const config = {
theme: "dark",
language: "en",
} satisfies AppConfig;
// 如果配置不符合 AppConfig,这里会直接报错
2、利用 typeof 复用类型
有时候我们定义了一个复杂的对象,但随后又想根据这个对象生成一个 interface。与其手写一遍几乎一模一样的结构,不如直接用 typeof。
TypeScript
const user = {
id: 1,
name: "John",
};
// 直接复用对象的结构,不需要再手动写一遍 Interface
type User = typeof user;
3、用 keyof 提取键名
在维护大型 interface 时,如果需要定义一组字段的集合,千万别手动去写 type Fields = "id" | "name" | ...。这种方式一旦后续 interface 增加了属性,你还得手动去更新这个 type,非常容易漏掉。
TypeScript
interface User {
id: number;
name: string;
email: string;
}
// 自动获取所有属性名,增加新属性时这里会自动同步
type UserFields = keyof User;
4、利用 as const 固定字面量
在定义常量配置时,TypeScript 默认会把值推断为宽泛的 string 类型。但在处理 Redux 的 action 或者一些状态机时,我们需要它保持精确的字面量值。
TypeScript
// 没有 as const 时,STATUS.SUCCESS 的类型是 string
const STATUS = {
SUCCESS: "success",
};
// 使用 as const 后,STATUS.SUCCESS 的类型就是 "success"
const STATUS = {
SUCCESS: "success",
} as const;
5、能推断就不要手动标类型
这是一个很小的习惯,但能让代码看起来清爽很多。TypeScript 的推断能力其实非常强,没必要在每个地方都写上 const count: number = 10;。
TypeScript
// 没必要这样写
const count: number = 10;
// 直接写,编译器自己心里有数
const count = 10;
二、灵活运用类型工具:应对业务场景
在处理 API 数据或组件 Props 时,Record、Partial、Pick 和 Omit 是绝对的生产力工具。
1、使用 Record 构建映射关系
当你需要定义一个键值对对象,且键的范围是固定的(比如各种状态的颜色配置)时,Record 非常好用。它能强制要求你必须实现所有的键,避免漏掉某个状态。
TypeScript
type Status = "success" | "warning" | "danger";
// 强制要求你必须写全这三种状态对应的颜色
const colors: Record<Status, string> = {
success: "#22c55e",
warning: "#f59e0b",
danger: "#ef4444",
};
2、使用 Partial 处理局部更新
在做编辑页面或者 PATCH 请求时,我们通常只需要提交部分字段。这时候用 Partial 可以快速生成一个"所有属性都变为可选"的类型。
TypeScript
interface User {
id: number;
name: string;
email: string;
}
// type UpdateUser = { id?: number; name?: string; email?: string; }
type UpdateUser = Partial<User>;
const updateUser = (data: UpdateUser) => {
// ...
};
3、用 Pick 和 Omit 精简接口
有时候一个 interface 太重了(包含了敏感信息或不需要的字段),我们可以通过 Pick(挑选)或 Omit(剔除)来快速得到一个轻量版的类型。
TypeScript
interface User {
id: number;
name: string;
email: string;
password: string;
}
// 只要 email 和 password,用于登录
type LoginUser = Pick<User, "email" | "password">;
// 排除掉 password,用于公开展示
type PublicUser = Omit<User, "password">;
三、进阶实战:通用组件与严谨逻辑
1、通过 Generics 构建高度复用的组件
如果你写了一个 Select 选择框组件,希望它既能渲染用户列表,又能渲染商品列表,那么 Generics(泛型)就是唯一的选择。
TypeScript
interface SelectProps<T> {
items: T[];
renderItem(item: T): React.ReactNode;
}
// 这样写,无论传入什么类型的 items,组件都能精准捕获类型
function MySelect<T>({ items, renderItem }: SelectProps<T>) {
return (
<ul>
{items.map((item, idx) => (
<li key={idx}>{renderItem(item)}</li>
))}
</ul>
);
}
2、用联合类型规避错误的 Prop 组合
这是一个很有意思的场景。假设你写一个 Button 组件,它要么是一个链接(有 href 属性),要么是一个普通按钮(有 onClick 属性),但逻辑上不应该两者都有。
如果用传统的可选属性 href?: string; onClick?: () => void;,开发者可能会同时传这两个属性,导致逻辑混乱。我们可以用联合类型来锁定这种"互斥"关系。
TypeScript
type ButtonProps =
| { href: string; onClick?: never }
| { onClick: () => void; href?: never };
// 这样定义后:
// <Button href="/home" /> -> OK
// <Button onClick={handleClick} /> -> OK
// <Button href="/home" onClick={handleClick} /> -> Error! (TS 会报错)
最后
TypeScript 的核心价值不仅仅在于抓 Bug,更在于它提供了一种方式,让我们能用"声明式"的方式去描述业务逻辑和组件边界。
这 10 个技巧不需要一次性全部学会,我建议大家在开发过程中,当你觉得自己在写重复的类型定义,或者觉得某个组件的 Props 边界模糊时,就回过头来看看这套方案。
如果你觉得这篇内容对你有帮助,欢迎在评论区交流。
如果文章对您有所帮助, 请不吝点击关注一下我的微信公众号:FSA全栈行动, 这将是对我最大的激励. 公众号不仅有Android技术, 还有iOS, Python等文章, 可能有你想要了解的技能知识点哦~