我曾经是那种典型的 JavaScript 原教旨主义者。
“类型?不就是多此一举吗?”
“写代码本来就应该自由一点。”
“等运行时报错再改也不迟。”
直到有一天,我们组在完成课业时,我调用了一个同学写的函数:
updateUser(id, status);
而我传的 true 被当作字符串 'true' 我以为 status 是个布尔值,传了 true。结果同学写的逻辑是接收字符串 'active' 或 'inactive'。存进了数据库……最后导出名单时,全班多了几十个状态为 "true" 的记录。
测试时没人发现,因为大家只测了自己的分支。等到老师来查签到率时,数据对不上,我们才手忙脚乱地翻代码,花了整整一晚修复。
那一刻我才意识到:JavaScript 的“灵活”,其实是把错误从开发阶段推迟到了生产环境。
而真正让我转变观念的,是一次用 React + TypeScript 搭建 TodoList 的经历。今天我想以一个刚转型 TS 的初学者视角,讲讲我是如何从怀疑、抵触,最终变成“真香党”的全过程。
一、JavaScript 的“自由”背后,藏着多少坑?
我们先看一段简单的加法函数:
function add(a, b) {
return a + b;
}
const res = add(10, '5');
console.log(res); // 输出 "105"
语法上完全合法,控制台也不会报错。但你本意是做数学运算,结果却变成了字符串拼接。
这就是 JavaScript 作为弱类型动态语言的典型问题:
- 不限制参数类型;
- 运行时才确定行为;
- 错误只能在执行时被发现。
小项目无所谓,可一旦团队协作或项目变大,这种“惊喜”就会越来越多。
比如有人传了个对象进来,有人把数字写成字符串,甚至 null 和 undefined 都能混进去……最后谁也不敢轻易改动旧代码,怕牵一发而动全身。
二、TypeScript 第一次救我于水火
后来我决定尝试 TypeScript。第一反应是:“这也太啰嗦了吧?”
比如这个函数要写成这样:
function addTs(a: number, b: number): number {
return a + b;
}
addTs(10, 5); // ✅ 正确
// addTs('10', '5'); // ❌ 编译报错:类型“string”不能赋给“number”
一开始觉得烦,但很快我就尝到了甜头。
某天我想调用一个别人写的工具函数:
formatDate(date: Date, format: string): string
光看这一行,我就知道必须传一个 Date 实例和格式化模板。如果我传了个字符串时间进去,编辑器立刻标红提醒。
这不是束缚,而是保护。
它让我在写代码的时候就能发现问题,而不是等到点击按钮才弹出 date.getFullYear is not a function。
三、TypeScript 给我的五个认知刷新
1. 类型不是枷锁,而是契约
以前我和同事对接组件,靠的是口头约定或者注释:
// props: { user: { name, age }, onEdit }
现在我直接用接口定义清楚:
interface IUser {
name: string;
age: number;
readonly id: number; // 只读,防止误改
hobby?: string[]; // 可选,避免判空遗漏
}
只要对方用了这个类型,IDE 就会自动提示该传什么、有哪些字段、哪些可选。
连文档都省了。
2. any 是救命稻草,但不能当饭吃
初学 TS 最容易犯的错就是到处写 any:
let data: any = fetchData();
data.hello(); // 能编译通过,但 runtime 可能崩溃
这等于放弃了类型检查,相当于“我会开挖掘机,请让我横着走”。
后来我知道了更安全的选择 —— unknown:
let data: unknown = fetchData();
if (typeof data === 'object' && data !== null && 'name' in data) {
console.log((data as { name: string }).name);
}
unknown 允许接收任何值,但在使用前必须做类型判断。这是一种负责任的“不确定”。
3. 枚举让状态更清晰
以前处理请求状态,我喜欢用魔法数字或字符串:
if (status === 1) { /* success */ }
if (status === 'loading') { /* show loading */ }
现在我用枚举:
enum Status {
Pending,
Success,
Failed
}
let s: Status = Status.Pending;
s = Status.Success; // 语义明确,不易写错
不仅代码可读性强了,重构时还能全局搜索引用,不怕漏改。
4. 泛型:让函数支持多种类型,又不失安全
我写了一个存取本地数据的工具:
function getStorages<T>(key: string, defaultValue: T): T {
const value = localStorage.getItem(key);
return value ? JSON.parse(value) : defaultValue;
}
这里的 T 是泛型,表示“你传啥类型,我就返回啥类型”。
我可以这样用:
const count = getStorages<number>('count', 0);
const todos = getStorages<Todo[]>('todos', []);
既复用了逻辑,又保留了类型推导。这才是真正的“高内聚、低耦合”。
5. 自定义类型别名,提升代码组织能力
随着项目变大,重复的结构越来越多。TS 提供了 type 来抽象这些模式:
type ID = string | number;
type User = {
name: string;
age: number;
hometown: string;
};
比起直接写 { name: string; age: number },这种方式更容易维护,也方便统一修改。
四、实战:用 TypeScript 写一个 TodoList
带着这些新认知,我重写了那个熟悉的 Todo 应用。整个过程像是在搭积木 —— 每一块都有明确的位置和形状。
第一步:定义核心数据结构
// types/todo.ts
export interface Todo {
id: number;
title: string;
completed: boolean;
}
状态是应用的核心,TS 让它不再随意变更。
第二步:封装自定义 Hook
// hooks/useTodos.ts
export function useTodos() {
const [todos, setTodos] = useState<Todo[]>(getStorages('todos', []));
useEffect(() => {
setStorages('todos', todos);
}, [todos]);
const addTodo = (title: string) => {
const newTodo: Todo = {
id: +Date.now(),
title,
completed: false,
};
setTodos([...todos, newTodo]);
};
const deleteTodo = (id: number) => {
setTodos(todos.filter(todo => todo.id !== id));
};
const toggleTodo = (id: number) => {
setTodos(
todos.map(todo =>
todo.id === id ? { ...todo, completed: !todo.completed } : todo
)
);
};
return { todos, addTodo, deleteTodo, toggleTodo };
}
这里每一处都受益于类型系统:
useState<Todo[]>明确了状态结构;- 参数
id: number防止传错类型; - 返回的对象结构清晰,消费方无需猜测。
第三步:组件之间安全通信
父子组件通过接口传递数据:
// components/TodoInput.tsx
interface Props {
onAdd: (title: string) => void;
}
const TodoInput: React.FC<Props> = ({ onAdd }) => {
const [inputValue, setInputValue] = useState('');
const handleSubmit = () => {
if (!inputValue.trim()) return;
onAdd(inputValue); // 类型已约束,确保传的是字符串
setInputValue('');
};
return (
<div>
<input value={inputValue} onChange={e => setInputValue(e.target.value)} />
<button onClick={handleSubmit}>Add</button>
</div>
);
};
子组件 TodoItem 同样有严格的输入规范:
interface Props {
todo: Todo;
onDelete: (id: number) => void;
onToggle: (id: number) => void;
}
一旦父组件传错字段,编译器立刻报错。再也不用担心“为什么删不掉?哦原来我把 onDelete 写成 onRemove 了”。
五、我的心态转变:从“不信赖”到“依赖”
回想刚开始接触 TS 时的心态:
“又要写类型,效率更低了。”
“反正最后都编译成 JS,何必多此一举?”
但现在我已经离不开它了。
TS 并没有让我写更多代码,而是让我写得更少、更准。
它帮我发现了那些原本需要调试半小时才能找到的低级错误;
它让团队协作变得更顺畅,因为每个人都能看清彼此的意图;
它让重构变得大胆,因为我可以放心地重命名、删除、调整结构,而不怕破坏已有功能。
更重要的是,它给了我一种“安心感”。
就像开车系上了安全带。虽然平时感觉不到它的存在,但关键时刻,它是唯一的依靠。
六、给还在犹豫的你几点建议
-
不要一开始就追求完美类型
可以先从关键函数和组件开始加类型,逐步推进。 -
善用 IDE 的智能提示
VSCode 对 TS 支持极好,鼠标悬停就能看到类型定义,极大提升阅读效率。 -
拒绝滥用
any
它像抗生素,偶尔用可以,长期依赖会破坏系统的免疫力。 -
把类型当作文档来写
一个好的接口定义,胜过千字说明。 -
理解编译时检查的价值
所有在开发阶段拦截的 bug,都是省下的时间和成本。
结语:从“随便写”到“安心写”
JavaScript 教会我快速实现想法,TypeScript 教会我如何长久维护它们。
如果说 JS 是一把锋利的刀,那 TS 就是给它加上了刀鞘和护手。
我不再害怕接手别人的代码,也不再担心自己写的代码半年后看不懂。
因为每一块逻辑都被类型牢牢锚定,每一处调用都有据可循。
如果你还在犹豫要不要学 TypeScript,我的建议是:
试试吧,哪怕只是为了少加几个 console.log() 去猜变量类型。
当你第一次在保存文件时就看到红色波浪线提醒错误,你会明白——
原来编程,真的可以不用靠运气。