前言: 最初接触 TypeScript 时,我并不理解为什么要给灵活的 JavaScript 套上"枷锁"。直到在维护复杂度逐渐提升的项目时,被层出不穷的类型错误困扰,才意识到类型系统带来的确定性有多重要。
最近系统学习了 TypeScript,并动手实现了一个经典的 Todo List Demo。这篇文章记录了我从理论到实践的学习过程,以及其中的一些思考。
一、 为什么我们要"自找麻烦"写 TS?
很多新手(包括以前的我)在安装完 TS 后,第一反应往往是: "写个代码还要先定义类型,这不是脱裤子放屁吗?"
但结合我的系统学习,TS 带来的爽点主要集中在三个方面:
- 上帝视角的 Bug 检测:JS 只有在运行时才会报错,而 TS 在你代码写下的那一刻,红色波浪线就开始预警了。这就好比有一个资深大佬坐在你旁边并在你按回车前说:"兄弟,这行代码会崩。"
- 智能提示(IntelliSense) :不用再凭记忆猜对象的属性名了,
.一下,所有可用的属性和方法全出来了。这才是真正的"代码补全"。 - 重构的底气:修改别人的代码或者几个月前的屎山时,改了接口字段,TS 会明确告诉你哪些文件受到了影响,不用全网搜字符串。
二、 核心概念速览
在进入实战前,我们先快速过一下笔记中的几个关键点,这些在后面的 Demo 中都会用到。
1. 基础类型与"救命稻草" Any
TS 的基础类型其实就是对 JS 的约束。
TypeScript
typescript
let age: number = 18;
let name: string = 'Tom';
// 数组泛型,这个写法很常见
let list: Array<string> = ['todo1', 'todo2'];
重点说说 any 和 unknown。 刚开始写 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 是如何渗透到开发的每一个环节的:
- Utils 层 :利用泛型实现了高度可复用的存储逻辑。
- Types 层 :利用接口定义了数据契约,防止字段拼写错误。
- Components 层 :利用
React.FC和Props接口,确保了组件通信的安全性。
从 JS 转到 TS,确实存在"阵痛期" 。你要多写很多代码,要处理很多类型报错。但正如我在笔记里记下的: "TypeScript 是强类型静态语言,它能让你编写出更干净、更健壮的代码。"
这是一种"长期主义"的胜利。当你接手一个庞大的项目,或者隔了一个月再看自己的代码,发现每一个变量的含义都清晰可见,每一次重构都信心满满时,你会由衷地感叹:
TypeScript,真香!
写在最后: 学习 TS 是一场持久战,不要因为繁琐的配置和报错而劝退。先从最简单的类型定义开始,慢慢尝试泛型和高级类型。