TypeScript实战中那些你不得不知道的类型优化技巧
我的写法,亲测靠谱
我用 TypeScript 写移动端项目快五年了,踩过不少坑,也摸索出一套自己觉得顺手的写法。很多人一上来就想着把类型系统玩到极致,结果代码越写越绕,最后连自己都看不懂。其实 TS 的核心价值不是炫技,而是减少低级错误、提升协作效率。下面这些是我日常开发中反复验证过的做法。
首先,接口定义别偷懒。我见过太多人直接用 any 或者 {} 敷衍了事,尤其是对接后端 API 的时候。这种写法短期内省事,后期改字段或者排查问题时能让你哭出来。
我的习惯是:每个 API 响应都配一个明确的 interface。比如:
interface UserResponse {
id: number;
name: string;
avatar?: string; // 可选字段明确标出
isActive: boolean;
}
// 调用时
const res = await fetch('https://jztheme.com/api/user/123');
const user: UserResponse = await res.json();
这里注意:可选字段一定要用 ? 标明。别图省事全写成必填,后端某个字段偶尔为空,你的页面就白屏了。我之前在一个活动页就这么栽过,上线后部分用户头像不显示,查了半天发现是 avatar 字段有时是 null,但 TS 类型里没标可选,导致运行时报错中断。
这几种错误写法,别再踩坑了
下面这些反面教材,我在 code review 里高频看到,新手老手都容易犯。
- 滥用
as any:这是最危险的操作。有人遇到类型报错第一反应就是加as any,看起来问题解决了,其实是把雷埋得更深。我建议:宁可多花十分钟写对类型,也不要动as any。实在不行,用unknown+ 类型守卫过渡。 - 函数返回值不写类型:TS 能自动推导,但显式声明更安全。尤其是异步函数,很多人写成
async function fetchData() { ... },结果返回值可能是Promise<User | null>,调用方不知道要判空。我强制要求团队所有函数必须显式标注返回类型。 - 用
Object或Array当类型:比如const data: Object = {}。这等于没写类型!应该用Record<string, unknown>或具体结构。
举个真实例子:有个同事写了个工具函数处理表单数据,用了 data: Object,结果传入 null 时没报错(因为 JS 里 null 是 object),运行时直接崩了。改成 Record<string, string | number> 后,编译器立刻提示传参错误。
实际项目中的坑
在移动端项目里,TS 和动态行为结合时特别容易出问题。比如你用 React 写一个下拉刷新组件,状态可能有 idle、loading、success、error 几种,这时候别用字符串字面量硬编码。
我一开始也图快,直接写:
const [status, setStatus] = useState('idle'); // 错!
结果某次手误写成 setStatus('loadin')(少个 g),TS 居然不报错,因为字符串类型太宽泛了。后来改成枚举或联合类型:
type RefreshStatus = 'idle' | 'loading' | 'success' | 'error';
const [status, setStatus] = useState<RefreshStatus>('idle');
现在只要拼错,编辑器立马红波浪线,省心多了。
另一个坑是事件处理。移动端经常要处理 touch 事件,比如 onTouchStart。很多人这样写:
const handleTouchStart = (e) => {
console.log(e.touches[0].clientX);
};
TS 默认不知道 e 是什么类型,会报错。正确做法是指定类型:
const handleTouchStart = (e: React.TouchEvent) => {
console.log(e.touches[0].clientX); // 安全访问
};
如果是原生 JS 环境(比如用 Vue 或纯 JS),就得用 TouchEvent:
element.addEventListener('touchstart', (e: TouchEvent) => {
// ...
});
别小看这个细节,我之前在一个 H5 项目里漏了类型,结果在 iOS 上某些机型 touches 是空数组,直接报 Cannot read property 'clientX' of undefined,线上事故。
关于泛型:别过度设计
泛型很强大,但很多人一上来就想搞个“万能 Hook”或者“通用请求封装”,结果类型参数套三层,维护成本爆炸。我的原则是:只在真正需要复用且类型不确定时才用泛型。
比如封装一个简单的数据请求 Hook:
// 别这么写(过度抽象)
function useApi<T>(url: string): { data: T | null; loading: boolean } {
// ...
}
// 我的实际写法:针对具体场景
interface User {
id: number;
name: string;
}
function useUser(id: number) {
const [user, setUser] = useState<User | null>(null);
// 具体逻辑...
return { user };
}
除非你真的要做一个跨项目的请求库,否则为每个业务模块写专用 Hook 更清晰。泛型不是银弹,用不好反而增加理解成本。
当然,有些地方泛型必不可少。比如工具函数:
function pick<T, K extends keyof T>(obj: T, keys: K[]): Pick<T, K> {
return keys.reduce((acc, key) => {
acc[key] = obj[key];
return acc;
}, {} as Pick<T, K>);
}
这种通用工具函数,泛型能让类型精准传递,值得投入。
最后的小建议
别追求 100% 类型覆盖。有些动态场景(比如从 URL 解析参数),硬要写死类型反而麻烦。我通常用 zod 或 io-ts 做运行时校验,TS 负责静态检查,两者互补。
另外,开启 strict 模式。虽然初期会有很多报错,但长期看能避免大量隐患。我现在的项目 tsconfig.json 里基本全开:
{
"compilerOptions": {
"strict": true,
"noImplicitAny": true,
"strictNullChecks": true,
"strictFunctionTypes": true
}
}
折腾完前两周确实头疼,但之后代码健壮性明显提升,尤其多人协作时,类型就是最好的文档。
以上是我踩坑后的总结,希望对你有帮助。有更好的方案欢迎评论区交流,比如你们怎么处理复杂的表单类型?我还在找更优雅的方式。

暂无评论