从“随便写”到“安心写”:一个 JavaScript 开发者与 TypeScript 的和解之路

6 阅读7分钟

我曾经是那种典型的 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 作为弱类型动态语言的典型问题:

  • 不限制参数类型;
  • 运行时才确定行为;
  • 错误只能在执行时被发现。

小项目无所谓,可一旦团队协作或项目变大,这种“惊喜”就会越来越多。

比如有人传了个对象进来,有人把数字写成字符串,甚至 nullundefined 都能混进去……最后谁也不敢轻易改动旧代码,怕牵一发而动全身。


二、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 并没有让我写更多代码,而是让我写得更少、更准。
它帮我发现了那些原本需要调试半小时才能找到的低级错误;
它让团队协作变得更顺畅,因为每个人都能看清彼此的意图;
它让重构变得大胆,因为我可以放心地重命名、删除、调整结构,而不怕破坏已有功能。

更重要的是,它给了我一种“安心感”

就像开车系上了安全带。虽然平时感觉不到它的存在,但关键时刻,它是唯一的依靠。


六、给还在犹豫的你几点建议

  1. 不要一开始就追求完美类型
    可以先从关键函数和组件开始加类型,逐步推进。

  2. 善用 IDE 的智能提示
    VSCode 对 TS 支持极好,鼠标悬停就能看到类型定义,极大提升阅读效率。

  3. 拒绝滥用 any
    它像抗生素,偶尔用可以,长期依赖会破坏系统的免疫力。

  4. 把类型当作文档来写
    一个好的接口定义,胜过千字说明。

  5. 理解编译时检查的价值
    所有在开发阶段拦截的 bug,都是省下的时间和成本。


结语:从“随便写”到“安心写”

JavaScript 教会我快速实现想法,TypeScript 教会我如何长久维护它们。

如果说 JS 是一把锋利的刀,那 TS 就是给它加上了刀鞘和护手。

我不再害怕接手别人的代码,也不再担心自己写的代码半年后看不懂。
因为每一块逻辑都被类型牢牢锚定,每一处调用都有据可循。

如果你还在犹豫要不要学 TypeScript,我的建议是:
试试吧,哪怕只是为了少加几个 console.log() 去猜变量类型。

当你第一次在保存文件时就看到红色波浪线提醒错误,你会明白——
原来编程,真的可以不用靠运气。