news 2026/6/10 19:21:05

TypeScript深度思考:一个TodoList项目教会你的不仅是语法

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
TypeScript深度思考:一个TodoList项目教会你的不仅是语法

引言

在学习 TypeScript 的初期,很多开发者会陷入一个误区:认为 TS 只是给变量加了个“后缀”(比如: string)。然而,当你真正接手一个中后台项目,或者像文中这样的 TodoList 实战时,你会发现 TS 的灵魂在于定义契约(Contract)

这个 TodoList 项目虽然功能简单,但它完美地涵盖了数据定义状态管理本地存储组件通信这四大核心场景。通过分析这段代码,我们将一起揭开 TS 在实际开发中的面纱。


数据契约的基石 —— Interface 与 Type

场景重现:
在项目中,我们需要管理待办事项(Todo)。在 JS 中,我们直接用对象;但在 TS 中,我们必须先定义这个对象“长什么样”。

代码解析:

// 定义数据状态的接口 export interface Todo { id: number; title: string; completed: boolean; }

核心干货:

  1. Interface vs Type:虽然两者在定义对象时非常相似,但在这个场景下,interface是首选。因为interface支持声明合并(Declaration Merging)。如果未来你需要扩展这个Todo(比如增加一个priority字段),你可以通过重新声明interface来扩展它,而不需要修改原始定义。
  2. 必选与可选:注意这里的属性都是必填的。如果你的 Todo 数据中某些字段可能不存在,记得加上?,例如description?: string
答疑解惑

Q: 为什么一定要定义这个接口?直接用any不行吗?
A: 看起来定义接口增加了代码量,但它带来了巨大的收益:

  • 智能提示(IntelliSense):当你写todo.的时候,编辑器会立刻提示你有idtitlecompleted可选,而不是让你盲打。
  • 编译时检查:如果你不小心写了todo.name,TS 会在编译阶段报错,而不是等到运行时报undefined

泛型(Generics)的魔法 —— 摆脱 any

场景重现:
在处理localStorage时,我们面临一个经典问题:localStorage只能存字符串,读取时需要反序列化。如果不用泛型,我们很容易写出any

代码解析:

// T 类型参数, 类型参数 export function getStorage<T>(key: string, defaultValue: T): T { const value = localStorage.getItem(key); return value ? JSON.parse(value) : defaultValue; }

核心干货:

  1. 泛型的本质:这里的<T>就像一个占位符。当你调用getStorage<Todo[]>('todos', [])时,TS 会自动推断出这个函数的返回值是Todo[]类型。
  2. 类型守卫:通过泛型,我们保证了输入什么类型,就输出什么类型。这比写死return JSON.parse(value) as Todo[]更加安全,因为这个函数可以复用于任何类型(User、Config 等)。
答疑解惑

Q: 为什么这里不直接写JSON.parse(value) as Todo[]
A: 因为getStorage是一个通用工具函数。如果写死了Todo[],下次你想存用户信息(User)时,就必须再写一个getUserStorage。泛型让这个函数拥有了“万能钥匙”的能力,是 TS 复用性的最高体现。


React 组件通信的类型安全

场景重现:
父子组件通过 Props 传递数据。子组件(TodoItem)需要接收父组件(TodoList)传来的数据和方法。

代码解析:

interface Props { todo: Todo; onToggle: (id: number) => void; onRemove: (id: number) => void; } const TodoItem: React.FC<Props> = ({ todo, onToggle, onRemove }) => { ... }

核心干货:

  1. 函数类型的定义:不要使用Function类型!Function只表示“这是一个函数”,但你不知道它需要什么参数。使用(id: number) => void可以精确约束函数签名。
  2. React.FC (Function Component):虽然代码中使用了React.FC,但在社区中有一种趋势是不使用React.FC。因为React.FC默认包含了children属性,如果你的组件不使用children,这反而会造成类型污染。新手建议先掌握这种写法,后续再了解PropsWithChildren
答疑解惑

Q: 为什么onToggleonRemove的参数是(id: number),而不是直接传整个todo对象?
A: 这是 React 性能优化的考量。如果传整个对象,子组件依赖的是对象引用。在父组件重新渲染时,如果对象是新生成的(即使内容没变),子组件也会强制更新。只传id(基本类型),配合useCallback,可以更好地控制子组件的重渲染。


Hooks 与 初始化逻辑的类型推断

场景重现:
在自定义 HookuseTodos中,我们不仅要管理状态,还要处理本地存储的读取。

代码解析:

const [todos, setTodos] = useState<Todo[]>( () => getStorage<Todo[]>(STORAGE_KEY, []) );

核心干货:

  1. 显式泛型标注:虽然 TS 通常能自动推断类型,但在useState的初始化函数中,显式标注<Todo[]>是一种良好的防御性编程习惯。这确保了setTodos的参数类型也被锁定为Todo[]
  2. 副作用依赖useEffect的依赖项[todos],因为todos有了明确的类型,TS 能防止你遗漏依赖,或者错误地放入了非原始类型的依赖导致死循环。

牛刀小试

1. 在getStorage函数中,如果localStorage里的数据被篡改了(比如变成了字符串而不是数组),TS 能检测出来吗?
  • 考察点:TS 的静态类型与运行时安全的区别。

  • 参考回答

    • “TS 只在编译时检查,无法检测运行时的数据篡改。
    • 进阶方案:在真实项目中,我会结合运行时类型检查库(如io-tszod)。在getStorage内部对JSON.parse的结果进行二次校验,如果不符合Todo[]的结构,就回退到默认值,从而保证程序的健壮性。”
2. 在useTodos中,为什么要写useState<Todo[]>?不写泛型行不行?
  • 考察点:类型推断的边界。

  • 参考回答

    • “如果不写,TS 会根据默认值[]推断出类型为never[]或者根据getStorage的泛型推断。虽然通常能推断正确,但显式声明是一种**自文档化(Self-documenting)**的行为。
    • 它明确告诉维护者:‘这个 State 必须是 Todo 数组’。这符合 TS 的核心原则——让错误在编码阶段暴露,而不是在重构时才发现。”
3.import type和普通的import有什么区别?你为什么在代码里用了import type
  • 考察点:ES Module 打包与类型擦除。

  • 参考回答

    • import type仅在编译阶段使用,不会生成任何 JavaScript 代码(会被‘擦除’)。
    • 使用它的主要目的是防止循环依赖减少打包体积。如果 A 文件 import 了 B 的类型,B 又 import 了 A 的类型,使用import type可以打破这种循环,因为类型在运行时是不存在的。”

结语

通过这个 TodoList 项目,希望你能明白 TypeScript 不仅仅是“加类型”,而是一种思维方式的转变。从“我打算怎么写代码”转变为“我定义了什么规则,代码必须遵守规则”。

不要害怕报错,每一次 TS 的报错提示,都是它在教你如何写出更健壮的代码。继续加油!

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/6/10 11:08:35

创客匠人伦理深研:知识变现中的数据安全与AI智能体边界——构建可信、可持续的知识服务生态

在AI智能体深度融入知识变现的今天&#xff0c;一个关乎行业存续的根本问题日益凸显&#xff1a;当用户将职业困惑、健康数据、学习轨迹托付于知识IP&#xff0c;我们如何守护这份沉甸甸的信任&#xff1f;《2026中国知识服务数据安全白皮书》警示&#xff1a;68%的用户因“担心…

作者头像 李华
网站建设 2026/6/10 7:49:35

BP神经网络信息新陈代谢模型

BP神经网络信息新陈代谢模型 1、BP神经网络是一种按照误差逆向传播算法训练的多层前馈神经网络&#xff0c;是应用最广泛的神经网络模型之一。 2、程序内容丰富&#xff0c;预测效果好&#xff0c;方便学习和推广 3、根据预测结果更新原始数据构成BP神经网络信息新陈代谢模型&a…

作者头像 李华
网站建设 2026/6/10 9:09:57

生成式AI测试框架的进化图谱:从自动化脚本到智能体协同

随着生成式AI&#xff08;Generative AI&#xff09;技术的成熟&#xff0c;软件测试领域正经历一场范式革命。传统基于确定性输入输出的测试方法&#xff08;如Selenium脚本&#xff09;已无法应对AI模型的概率性输出、动态上下文依赖和伦理安全边界等新挑战。2025年行业调研显…

作者头像 李华
网站建设 2026/6/9 18:50:22

Spring的生命周期管理

1. Spring Bean 生命周期概述 Spring Bean 生命周期是指 Spring 容器从创建一个 Bean 实例到销毁 Bean 实例这一过程中的一系列操作。整个生命周期包含以下几个关键阶段&#xff1a; Bean 实例化属性注入初始化销毁 每个阶段中&#xff0c;Spring 提供了钩子方法、回调接口以…

作者头像 李华
网站建设 2026/6/10 9:05:12

13 秒插入 30 万条数据,这才是批量插入正确的姿势!

01 30万条数据插入数据库验证 验证的数据库表结构如下&#xff1a; CREATETABLEt_user ( idint(11) NOTNULL AUTO_INCREMENT COMMENT用户id, usernamevarchar(64) DEFAULTNULLCOMMENT用户名称, ageint(4) DEFAULTNULLCOMMENT年龄,PRIMARY KEY (id) ) ENGINEInnoDBDEFAULTCHAR…

作者头像 李华
网站建设 2026/6/10 8:54:12

RAG 深度实践系列(六):基于科大讯飞 RAG + 星火知识库的企业级实战指南

目录一、 企业级 RAG 的落地挑战与科大讯飞的生态赋能1.1、 讯飞开放平台&#xff1a;RAG 的“大脑”与“算力”底座1.2、 星火知识库&#xff1a;私域知识向量化的工程实现二、 工程实践2.1、 应用创建与密钥管理2.2、 接口鉴权认证的底层逻辑与时间戳偏移处理2.3、 文档管理流…

作者头像 李华