【复习笔记】TypeScript 真的方便吗?类型定义到底该写在哪里?
摘要:在日常公司项目中,TypeScript (TS) 究竟是“真香”还是“累赘”?为什么感觉每个文件都在写类型?本文从实战角度深度剖析 TS 的优缺点,并给出类型定义的最佳存放策略,帮助开发者摆脱“类型焦虑”,写出既安全又清爽的代码。
📖 前言
最近在项目复盘中,团队里关于 TypeScript (TS) 的讨论再次热烈起来。
有的同事感叹:“重构时太爽了,编译器帮我找遍了所有报错!”
也有的同事吐槽:“太麻烦了,写个简单功能都要定义半天类型,感觉每个文件都被类型淹没了。”
TS 真的方便吗?类型到底应该写在哪里?
这篇博客是我对这两个核心问题的深度总结,适合正在犹豫是否全面上 TS,或者正在被类型系统困扰的开发者复习使用。
🟢 第一部分:灵魂拷问——TS 真的方便吗?
答案是:短期是“麻烦”,长期是“救命”;小项目是“累赘”,大项目是“基石”。
1. 什么时候觉得“真香”?(爽点)
- 🛡️ 重构时的底气
- JS 场景:修改一个底层字段名,心里发慌。必须全局搜索字符串,生怕漏掉某个动态调用的地方,只能靠测试覆盖。
- TS 场景:改完接口定义,编辑器直接标红所有受影响的文件。编译器帮你做了一次全量回归测试。跟着红线修,修完变绿,上线无忧。
- 💡 智能提示即文档
- 不需要翻找代码或询问同事“这个对象有哪些属性?”。鼠标悬停即可查看类型定义,输入
.自动补全。对于复杂的嵌套结构,效率提升巨大。
- 不需要翻找代码或询问同事“这个对象有哪些属性?”。鼠标悬停即可查看类型定义,输入
- 🤝 团队协作的契约
- 前后端联调时,TS 的
Interface就是契约。后端改了 API,前端编译直接报错,强制同步,避免了“后端偷偷改字段,前端线上崩盘”的惨剧。
- 前后端联调时,TS 的
2. 什么时候觉得“真烦”?(痛点)
- 🐢 初期开发速度稍慢
- 在快速原型(MVP)阶段,每写一个变量都要思考类型,确实会拖慢节奏。
- 🎭 “类型体操”过度设计
- 部分开发者喜欢炫技,写出极其复杂的泛型和条件类型,导致代码可读性极差,报错信息像天书。
- 📦 第三方库类型缺失
- 遇到没有类型定义的老旧库,需要手写
.d.ts或被迫使用any,此时会感到挫败。
- 遇到没有类型定义的老旧库,需要手写
⚖️ 结论对比表
| 维度 | JavaScript | TypeScript | 胜出者 |
|---|---|---|---|
| 编写速度 (初期) | 🚀 快 | 🐢 稍慢 | JS |
| 编写速度 (中后期) | 🐢 慢 (查文档/调试) | 🚀 快 (智能提示) | TS |
| Debug 成本 | 😫 高 (运行时错误) | 😌 低 (编译时拦截) | TS |
| 重构信心 | 😰 低 | 😎 高 | TS |
| 大型项目维护 | ❌ 灾难 | ✅ 必需 | TS |
💡 核心观点:TS 的麻烦是**“写在编译前的麻烦”,它帮你避免了“上线后半夜被电话叫醒修 Bug 的麻烦”**。
📂 第二部分:类型定义到底写在哪里?
很多新手觉得“每个文件都在写类型”,是因为缺乏分层策略。在成熟的工程中,类型定义遵循以下 4 层架构:
1. 📍 局部定义 (Local Scope)
- 位置:当前
.ts或.vue文件顶部。 - 适用场景:仅在当前组件/文件内部使用的数据结构。
- 原则:就近原则。不要为了复用而强行提取到外部,导致文件跳转混乱。
// src/components/UserCard.vue
<script setup lang="ts">
// ✅ 正确:只在内部用的临时结构,直接写在这里
interface LocalUser {
id: number;
name: string;
tempFlag: boolean;
}
const props = defineProps<{
user: LocalUser;
}>();
</script>
2. 🏛️ 集中管理 (Global Types)
- 位置:
src/types/或src/interfaces/目录。 - 适用场景:核心业务模型、后端返回数据、多组件共用的复杂对象。
- 原则:单一数据源。后端改字段,只需改此处,全项目报错提示。
目录结构示例:
src/
├── types/
│ ├── index.ts # 统一导出
│ ├── user.ts # 用户模块
│ ├── order.ts # 订单模块
│ └── api.d.ts # 全局 API 结构
代码示例 (src/types/user.ts):
export interface UserInfo {
id: number;
username: string;
role: 'admin' | 'user';
}
export interface UserApiResponse {
code: number;
data: UserInfo;
msg: string;
}
使用方式:
import type { UserInfo } from '@/types/user'; // 推荐加 type 关键字优化构建
3. 🤖 自动推断 (Inference)
- 位置:无需显式书写。
- 适用场景:简单变量、常量、逻辑清晰的函数返回值。
- 原则:能省则省。TS 非常聪明,不要画蛇添足。
// ❌ 没必要写
const count: number = 0;
const name: string = "Alice";
// ✅ 推荐写法 (让 TS 自己猜)
const count = 0; // 自动推断为 number
const list = [1, 2, 3]; // 自动推断为 number[]
// 函数返回值通常也能自动推断
function add(a: number, b: number) {
return a + b; // 自动推断返回 number
}
4. 🛠️ 全局声明与第三方 (Declaration Files)
- 位置:
src/types/shims-xxx.d.ts。 - 适用场景:扩展
window对象、声明图片资源、补充缺失的第三方库类型。
// src/types/shims-vue.d.ts
declare module '*.png' {
const content: string;
export default content;
}
declare global {
interface Window {
myCustomPlugin: any;
}
}
💡 第三部分:如何避免“类型淹没代码”?(最佳实践)
如果你觉得写类型很痛苦,请检查是否违反了以下原则:
1. 拒绝 any 滥用,但也别过度严谨
- 严禁到处写
any,这会让 TS 退化为 JS。 - 如果实在无法确定类型,先用
unknown,或者定义一个简单的 Interface。 - 不要追求 100% 完美的类型体操,可读性 > 类型严谨度。
2. 善用工具类型 (Utility Types)
不要重复定义相似的结构,基于现有类型进行“裁剪”:
interface User { id: number; name: string; email: string; password: string; }
// 注册时:去掉 id 和 password
type RegisterDto = Omit<User, 'id' | 'password'>;
// 编辑时:所有字段变为可选
type UpdateDto = Partial<User>;
// 只取 id 和 name
type UserSummary = Pick<User, 'id' | 'name'>;
3. 优先使用 interface 定义对象
interface支持声明合并(后期可追加属性)。- 报错信息通常比
type更友好。 - 仅在需要使用联合类型、元组或复杂映射时,才使用
type。
4. 组件 Props 单独提取
对于大型组件,将 Props 定义提取到独立接口,保持 <script> 区域清爽:
// ❌ 臃肿
const props = defineProps<{
id: number; name: string; /* ... 20 个属性 */
}>();
// ✅ 清爽
import type { UserCardProps } from './types';
const props = defineProps<UserCardProps>();
📝 总结
TypeScript 在现代前端开发中已经不是“可选项”,而是**“必选项”**。
-
类型分布策略:
- 简单变量 ➡️ 靠推断(不写)。
- 局部临时结构 ➡️ 写在文件内。
- 核心业务模型 ➡️ 集中在
src/types。 - 环境/资源 ➡️ 写在
.d.ts。
-
心态转变:
不要为了写类型而写类型。类型是为“人”服务的,它的目的是让代码更易读、更安全、更易维护。
当你习惯了有类型提示的开发,享受着重构时的安全感,你就再也回不去裸写 JavaScript 的日子了。
更多推荐
所有评论(0)