拒绝 "AnyScript":从 Todo List 实战看 TypeScript 的真香定律

前言: 最初接触 TypeScript 时,我并不理解为什么要给灵活的 JavaScript 套上"枷锁"。直到在维护复杂度逐渐提升的项目时,被层出不穷的类型错误困扰,才意识到类型系统带来的确定性有多重要。

最近系统学习了 TypeScript,并动手实现了一个经典的 Todo List Demo。这篇文章记录了我从理论到实践的学习过程,以及其中的一些思考。

一、 为什么我们要"自找麻烦"写 TS?

很多新手(包括以前的我)在安装完 TS 后,第一反应往往是: "写个代码还要先定义类型,这不是脱裤子放屁吗?"

但结合我的系统学习,TS 带来的爽点主要集中在三个方面:

  1. 上帝视角的 Bug 检测:JS 只有在运行时才会报错,而 TS 在你代码写下的那一刻,红色波浪线就开始预警了。这就好比有一个资深大佬坐在你旁边并在你按回车前说:"兄弟,这行代码会崩。"
  2. 智能提示(IntelliSense) :不用再凭记忆猜对象的属性名了,. 一下,所有可用的属性和方法全出来了。这才是真正的"代码补全"。
  3. 重构的底气:修改别人的代码或者几个月前的屎山时,改了接口字段,TS 会明确告诉你哪些文件受到了影响,不用全网搜字符串。

二、 核心概念速览

在进入实战前,我们先快速过一下笔记中的几个关键点,这些在后面的 Demo 中都会用到。

1. 基础类型与"救命稻草" Any

TS 的基础类型其实就是对 JS 的约束。

TypeScript

typescript 复制代码
let age: number = 18;
let name: string = 'Tom';
// 数组泛型,这个写法很常见
let list: Array<string> = ['todo1', 'todo2']; 

重点说说 anyunknown。 刚开始写 TS,遇到报错不想解,直接一把梭用 any

TypeScript

ini 复制代码
let data: any = 1; 
data = "hello"; // 没问题
data.map(); // 编译不报错,运行直接炸

any 是放弃治疗 ,它让 TS 退化回了 JS。 而 unknown 是更安全的 any

TypeScript

ini 复制代码
let safeData: unknown = 1;
// safeData.hello(); // 报错!在使用前必须强制做类型收窄或断言

心得少用 any,那是饮鸩止渴。

2. 接口(Interface)与类型别名(Type)

这是定义数据结构的灵魂。在实战中,我们通常用 Interface 来约定对象。

TypeScript

typescript 复制代码
interface User {
  readonly id: number; // 只读,创建后不可改,防止误操作
  name: string;
  age: number;
  hobby?: string; // 可选属性,不是每个人都有爱好
}

这比写文档管用多了,代码即文档。

3. 枚举(Enum)

笔记里提到了枚举,这在管理状态时非常有用,比如任务状态:

TypeScript

ini 复制代码
enum Status {
    Pending = 0,
    Success = 1,
    Failed = 2,
}
let currentStatus = Status.Pending;

这样代码里就不会出现莫名其妙的 "0" 或 "1" 这种魔术数字(Magic Number)。


三、 实战:构建一个类型安全的 Todo List

Talk is cheap, show me the code. 咱们来看看如何用 React + TS 搭建一个规范的项目。

1. 项目结构规划

好的结构是成功的一半。在这个 Demo 中,我采用了职责分离的模式:

Plaintext

scss 复制代码
src
 ├── components  // UI 组件 (纯展示,负责渲染和回调)
 ├── hooks       // 逻辑钩子 (负责状态管理和业务逻辑)
 ├── types       // 类型定义 (全局通用的类型契约)
 ├── utils       // 工具函数 (本地存储等)
 └── App.tsx     // 组装层

2. 定义核心类型 (The Contract)

一切始于类型。在 src/types/todo.ts 中,我们定义 Todo 对象的形状:

TypeScript

typescript 复制代码
export interface Todo {
  id: number;
  title: string;
  completed: boolean;
}

一旦这个接口定义好了,全项目的组件在调用 Todo 数据时,都会受到严格监管。

3. 泛型的妙用:封装 LocalStorage

TS 最强大的特性之一就是泛型(Generics) 。简单理解,泛型就是类型的参数

src/utils/storages.ts 中,我封装了 localStorage。因为我不确定存进去的是字符串、数字还是对象数组,所以我用 T 来代表未来的类型:

TypeScript

typescript 复制代码
// T 是一个占位符,调用的时候再决定它是什么
export function getStorage<T>(key: string, defaultValue: T): T {
    const value = localStorage.getItem(key);
    return value ? JSON.parse(value) : defaultValue;
}

export function setStorage<T>(key: string, value: T) {
    localStorage.setItem(key, JSON.stringify(value));
}

使用时的爽点

TypeScript

ini 复制代码
// TS 自动推断出 todos 是 Todo[] 类型,而不是 any
const todos = getStorage<Todo[]>('todos', []); 

4. 业务逻辑分离:Custom Hook

src/hooks/useTodos.ts 中,我们将状态逻辑抽离。这里要注意 useState 的泛型用法:

TypeScript

typescript 复制代码
import { useState, useEffect } from 'react';
import type { Todo } from '../types/todo';
import { getStorage, setStorage } from '../utils/storages';

const STORAGE_KEY = 'todos';

export function useTodos() {
  // 明确告诉 TS,这个 state 存的是 Todo 数组
  const [todos, setTodos] = useState<Todo[]>(() => 
    getStorage<Todo[]>(STORAGE_KEY, [])
  );

  useEffect(() => {
    setStorage<Todo[]>(STORAGE_KEY, todos);
  }, [todos]);

  const addTodo = (title: string) => {
    const newTodo: Todo = {
      id: +new Date(),
      title,
      completed: false
    };
    setTodos([...todos, newTodo]);
  };

  // ... toggleTodo 和 removeTodo 逻辑省略,原理同上

  return { todos, addTodo, toggleTodo, removeTodo };
}

这样做的好处是,App.tsx 变得极其干净,只负责组装。

5. 组件开发:Props 的类型约束

这是 React + TS 最常见的场景。父子组件传值,必须把 Props 定义清楚。

TodoList.tsx 为例:

TypeScript

typescript 复制代码
import * as React from 'react';
import type { Todo } from '../types/todo';
import TodoItem from './TodoItem';

// 定义组件需要接收什么,少传或错传都会报错
interface Props {
    todos: Todo[];
    onToggle: (id: number) => void; // 明确函数的参数和返回值
    onRemove: (id: number) => void;
}

// React.FC<Props> 显式声明这是一个函数式组件,且 Props 符合上面的接口
const TodoList: React.FC<Props> = ({ todos, onToggle, onRemove }) => {
    return (
        <ul>
            {todos.map((todo) => (
                <TodoItem
                    key={todo.id}
                    todo={todo}
                    onToggle={onToggle}
                    onRemove={onRemove}
                />
            ))}
        </ul>
    );
};
export default TodoList;

TodoInput.tsx 中,我们还处理了表单事件:

TypeScript

typescript 复制代码
interface Props {
  onAdd: (title: string) => void;
}

const TodoInput: React.FC<Props> = ({ onAdd }) => {
  const [value, setValue] = React.useState<string>('');

  const handleAdd = () => {
    if (!value.trim()) return;
    onAdd(value);
    setValue('');
  };

  return (
    <div>
      <input 
        value={value} 
        onChange={e => setValue(e.target.value)} // TS 知道 e 是 ChangeEvent
      />
      <button onClick={handleAdd}>添加</button>
    </div>
  );
};

四、 总结与思考

通过这个简单的 Demo,我们可以看到 TypeScript 是如何渗透到开发的每一个环节的:

  1. Utils 层 :利用泛型实现了高度可复用的存储逻辑。
  2. Types 层 :利用接口定义了数据契约,防止字段拼写错误。
  3. Components 层 :利用 React.FCProps 接口,确保了组件通信的安全性。

从 JS 转到 TS,确实存在"阵痛期" 。你要多写很多代码,要处理很多类型报错。但正如我在笔记里记下的: "TypeScript 是强类型静态语言,它能让你编写出更干净、更健壮的代码。"

这是一种"长期主义"的胜利。当你接手一个庞大的项目,或者隔了一个月再看自己的代码,发现每一个变量的含义都清晰可见,每一次重构都信心满满时,你会由衷地感叹:

TypeScript,真香!


写在最后: 学习 TS 是一场持久战,不要因为繁琐的配置和报错而劝退。先从最简单的类型定义开始,慢慢尝试泛型和高级类型。

相关推荐
UIUV3 小时前
React+Zustand实战学习笔记:从基础状态管理到项目实战
前端·react.js·typescript
用户12039112947263 小时前
从零起步,用TypeScript写一个Todo App:踩坑与收获分享
前端·react.js·typescript
南村群童欺我老无力.3 小时前
Flutter 框架跨平台鸿蒙开发-鸿蒙计算器开发教程
vscode·flutter·华为·typescript·harmonyos
qiqiliuwu3 小时前
VUE3+TS+ElementUI项目中监测页面滚动scroll事件以及滚动高度不生效问题的解决方案(window.addEventListener)
前端·javascript·elementui·typescript·vue
南村群童欺我老无力.4 小时前
Flutter 框架跨平台鸿蒙开发 - 从零开发经典推箱子游戏
flutter·游戏·华为·typescript·harmonyos
程序员的程4 小时前
我用 stock-sdk 做了个 A 股股票看板
前端·javascript·typescript
这个图像胖嘟嘟18 小时前
前端开发的基本运行环境配置
开发语言·javascript·vue.js·react.js·typescript·npm·node.js
kk晏然19 小时前
TypeScript 错误类型检查,前端ts错误指南
前端·react native·typescript·react
VT.馒头1 天前
【力扣】2622. 有时间限制的缓存
javascript·算法·leetcode·缓存·typescript