🎬 从「写崩三个项目」到「TypeScript 真香」:一个 Todo 列表的血泪救赎史


🎬 从「写崩三个项目」到「TypeScript 真香」:一个 Todo 列表的血泪救赎史

前言

你以为你在写 JavaScript,其实你在玩一场名为**「大家来找茬」**的生存游戏。

只不过,找茬的是运行时的用户,而被"公开处刑"的,是深夜加班的你。

直到有一天,一位带着蓝色徽章的骑士拍了拍你的肩膀:
「兄弟,别等了。编译时就能把错揪出来,不香吗?」


🌪️ 第一幕:崩溃之夜,undefined 的狂欢

时间 :凌晨 02:30
地点 :某互联网公司只剩一盏灯的工位
人物:小码(资深"背锅"工程师)

屏幕上,红色的报错信息像瀑布一样刷屏。小码盯着控制台,眼神空洞:

"为什么?!我明明写了 todo.title,为什么用户看到的全是 undefined?我的 Todo 应用变成了'未定义'应用了吗?!"

🎞️ 闪回镜头:

  • 同事 A (在群里):"我觉得字段叫 text 更直观,我就改啦!"
  • 同事 B (在后端):"哦,数据库里存的是 completed,不是 done,顺手改了下返回结构。"
  • 小码(天真地敲键盘):"没事没事,JS 嘛,灵活一点,大家都懂的~"

💥 现实暴击: 当用户点击「完成」按钮的那一刻,由于字段名对不上,整个页面瞬间崩塌。控制台里,Cannot read property 'length' of undefined 像在嘲笑小码的天真。

小码瘫坐在椅子上,内心 OS:

"要是......要是写代码的时候,就有个严厉的老师拿着红笔,在我写下 title 而不是 text 的瞬间狠狠打我的手,该多好啊......"

(图注:那一刻,小码觉得自己的职业生涯也要 undefined 了)


🦸‍♂️ 第二幕:蓝衣骑士降临,TypeScript 的真香现场

✨ 特效音叮!

一道柔和的蓝光闪过,TypeScript 君(戴着单片眼镜,手持类型检查权杖)凭空出现在显示器旁。

TypeScript 君 : "兄弟,别哭了。JavaScript 是自由的,但自由过了头就是灾难。让我来帮你建立秩序吧。在我的世界里,Bug 在代码'出厂'前就会被拦截,不用等用户投诉,你自己先'社死'一遍,好过上线后'社会性死亡'。"

小码半信半疑:"你......真的能拦住那些手滑的错误?"

TypeScript 君推了推眼镜:"不信?看好了!这是你的新武器------Interface(接口),也就是我们数据结构的'宪法'。"

typescript 复制代码
// 📜 数据结构宪法:types/todo.ts
export interface Todo {
  id: number;
  text: string;      // ⚠️ 统一叫 text,谁敢写 title 就报错!
  completed: boolean; // ⚠️ 必须是布尔值,字符串 "false" 也不行!
}

🔥 高潮时刻: 小码试着像以前一样手滑,写下了这段代码:

typescript 复制代码
const badTodo: Todo = {
  id: 1,
  title: "买菜",      // ❌ TypeScript 君立刻亮出红牌:属性 'title' 不存在!
  done: false         // ❌ TypeScript 君再次亮牌:属性 'done' 不存在,应该是 'completed'!
};

编辑器反应 :瞬间出现红色波浪线,鼠标悬停提示:"对象字面量只能指定已知属性,并且 'title' 不在类型 'Todo' 中。"

小码瞪大眼睛:"卧槽!还没运行就报错了?!"

TypeScript 君得意地笑:"这就叫静态类型检查。怎么样,香不香?"

(图注:TypeScript 君:有我在,undefined 休想靠近你的用户!)


🗺️ 第三幕:全景地图 ------ 一图看懂 TS 如何守护数据流

在 TypeScript 的加持下,我们的 Todo 项目不再是散沙,而是一座精密的堡垒。

text 复制代码
src/
├── types/todo.ts        👑【宪法】:定义 Todo 长什么样(唯一真理)
├── utils/storage.ts     🛡️【盾牌】:泛型封装 localStorage,拒绝 any
├── hooks/useTodos.ts    🧠【大脑】:状态核心,增删改查 + 类型贯穿
├── components/
│   ├── TodoInput.tsx    📥【入口】:Props 强约束,传错直接编译失败
│   ├── TodoList.tsx     📋【列表】:数组类型安全,子组件传参校验
│   └── TodoItem.tsx     🧩【单元】:最后一公里的数据守护
├── App.tsx              🏗️【总装】:类型闭环,完美组装
└── main.tsx             🚀【启动】:非空断言,自信挂载

💡 核心逻辑 : 数据从 TodoInput 流入,经过 useTodos 处理,存入 localStorage,最后由 TodoList 渲染。每一步都有类型契约,哪一环敢"越狱",编译器直接熔断!


🛠️ 第四幕:实战拆解 ------ 代码里的"真香"细节

4.1 📜 立法者:types/todo.ts

"一锤定音,从此再无歧义"

typescript 复制代码
// 数据状态是应用的心脏,TS 是心脏的保镖
export interface Todo {
  id: number;
  text: string;
  completed: boolean;
}
  • 真香点 :这是全项目的"单一事实来源"。以后谁敢加个 createdAt 字段却不更新这里?编译器第一个不答应!所有引用处都会提示你补全,重构从未如此丝滑。

4.2 🧙‍♂️ 魔法师:utils/storage.ts

"泛型 <T>:一套逻辑,通吃万物"

typescript 复制代码
export function getStorage<T>(key: string, defaultValue: T): T {
  const value = localStorage.getItem(key);
  // 哪怕读出来是字符串,我也能把它变成你想要的 T
  return value ? JSON.parse(value) : defaultValue;
}

export function setStorage<T>(key: string, value: T): void {
  localStorage.setItem(key, JSON.stringify(value));
}
  • 真香点
    • getStorage<Todo[]>('todos', []):告诉 TS"我读的是 Todo 数组",返回值自动推断为 Todo[]彻底告别 any 黑盒
    • 如果没有泛型,你得为 User、Config、Order 各写一套 get/set,现在?一套搞定!

4.3 🧠 指挥官:hooks/useTodos.ts

"类型贯穿始终,逻辑稳如老狗"

typescript 复制代码
import { useState, useEffect } from 'react';
import type { Todo } from '../types/todo'; // 👈 只导类型,打包更轻
import { getStorage, setStorage } from '../utils/storage';

const STORAGE_KEY = 'todos';

export function useTodos() {
  // 初始值从 localStorage 读,类型明确为 Todo[]
  const [todos, setTodos] = useState<Todo[]>(
    () => getStorage<Todo[]>(STORAGE_KEY, [])
  );

  // 只要 todos 变了,自动存回去,类型安全
  useEffect(() => {
    setStorage<Todo[]>(STORAGE_KEY, todos);
  }, [todos]);

  const addTodo = (text: string) => { // 👈 参数锁死为 string
    const newTodo: Todo = {
      id: +new Date(),
      text,
      completed: false
    };
    setTodos([...todos, newTodo]);
  };

  const toggleTodo = (id: number) => {
    // TS 能推断出 map 后的结果仍是 Todo[],因为结构符合 interface
    const newTodos = todos.map((todo) =>
      todo.id === id ? { ...todo, completed: !todo.completed } : todo
    );
    setTodos(newTodos);
  };
  
  // ... removeTodo 略

  return { todos, addTodo, toggleTodo, removeTodo };
}
  • 真香点import type 让构建产物更纯净;useState<Todo[]> 让状态管理不再靠猜。

4.4 📥 守门员:TodoInput.tsx

"Props 即文档,传错即报错"

typescript 复制代码
interface TodoInputProps {
  onAddTodo: (text: string) => void; // 👈 契约:我只要这个函数
}

export default function TodoInput({ onAddTodo }: TodoInputProps) {
  const [text, setText] = useState('');
  
  const handleSubmit = (e: FormEvent<HTMLFormElement>) => {
    e.preventDefault();
    const trimmed = text.trim();
    if (trimmed) {
      onAddTodo(trimmed); // ✅ 类型匹配,安全调用
      setText('');
    }
  };
  
  // ... JSX 略
}
  • 真香点 :父组件如果手滑传了 (id) => ...,或者忘了传 onAddTodo编译器直接红牌罚下,根本轮不到用户发现。

4.5 🧩 组装车间:App.tsx & main.tsx

"类型闭环与非空断言"

typescript 复制代码
// App.tsx:类型在顶层完美闭环
export default function App() {
  const { todos, addTodo, toggleTodo, removeTodo } = useTodos();
  return (
    <div>
      <TodoInput onAddTodo={addTodo} /> 
      {/* addTodo 的类型自动匹配 onAddTodo 的要求 */}
      <TodoList todos={todos} onToggleTodo={toggleTodo} onRemove={removeTodo} />
    </div>
  );
}

// main.tsx:自信的非空断言
createRoot(document.getElementById('root')!).render(<App />);
// 👆 '!' 告诉 TS:我发誓 root 一定存在,别啰嗦了!

🏆 第五幕:TS 护城河 ------ 功能与安全的完美映射

功能 实现位置 🛡️ TypeScript 如何保护你
添加待办 addTodo 参数锁死 string,想传数字?门都没有。
切换状态 toggleTodo map 返回类型自动推断,确保数组结构不变。
持久化 storage.ts 泛型保证读写类型一致,杜绝 JSON.parse 后的类型丢失。
组件通信 Props Interface 漏传、错传、类型不匹配?编译期直接拦截
DOM 操作 main.tsx 非空断言 ! 或可选链 ?.,让 null 检查更可控。

🎓 第六幕:新手入坑指南(三步走)

别被篇幅吓到,入门其实只需三步:

  1. 请神上身npm install -D typescript
  2. 立下规矩 :在 types/ 目录下写出你的第一个 interface
  3. 处处设防:给变量、函数参数、Props 加上类型注解。

记住这三句真言

  • Interface 是宪法,全项目照着做。
  • 泛型 <T> 是魔法,一次编写处处用。
  • 编译检查 是保镖,Bug 不过夜。

🎬 终幕:再见,undefined;你好,未来

场景:又是深夜,但这次办公室里充满了轻松的氛围。小码从容地提交代码,CI/CD 流水线上一路绿灯。

小码喝着咖啡,对着屏幕微笑:

"以前总觉得 TS 麻烦,要写好多类型定义。现在才发现,那些多写的几行代码,是我深夜安睡的保障,是团队沟通的契约,是线上稳定的基石。"

TypeScript 君(渐渐隐去,留下最后一句话):

"记住,TypeScript 不是银弹,但它能让你少写很多'运行时才发现'的 Bug。 从 Todo 开始,从今天开始------真香,从来不晚。"


💡 彩蛋:那些年我们踩过的 TS 坑(避坑指南)

  • Q: todo.title 还是 todo.text
    • A: 上 interface!一次定义,到处复用,谁改谁报错。
  • Q: localStorage 读出来全是 any
    • A: 泛型 getStorage<Todo[]> 安排上,类型瞬间清晰。
  • Q: document.getElementById 为啥报错?
    • A: 它可能返回 null。要么用 ! 断言(你确定有),要么先判空(更安全)。

👉 互动话题 : 你是在哪个瞬间决定"弃 JS 投 TS"的?是在修一个莫名其妙的 undefined 时,还是在接手一座"屎山"代码时? 欢迎在评论区分享你的**"真香"**故事!👇

相关推荐
向上的车轮4 小时前
TypeScript 一日速通指南:数据类型全解析与转换指南
javascript·typescript
Bling_Bling_16 小时前
TypeScript 核心知识点整理
typescript
我命由我123457 小时前
Element Plus - Cascader 观察记录(基本使用、动态加载、动态加载下的异常环境)
开发语言·前端·javascript·vue.js·typescript·html5·js
xiaoxue..9 小时前
前后端双令牌认证(Access Token + Refresh Token)全方案实现:安全与体验兼得
前端·后端·web安全·面试·typescript·nestjs
We་ct1 天前
LeetCode 79. 单词搜索:DFS回溯解法详解
前端·算法·leetcode·typescript·深度优先·个人开发·回溯
鹏北海1 天前
TypeScript 装饰器完全指南
前端·typescript
向上的车轮1 天前
TypeScript 一日速通指南:以订单管理系统实战为核心
前端·javascript·typescript
兆子龙1 天前
MyBatis-Plus 踩坑血泪史:我们踩过的那些坑
typescript