核心内容摘要
红桃C17·C18:解锁生命密码,开启健康新篇章
引言在学习 TypeScript 的初期很多开发者会陷入一个误区认为 TS 只是给变量加了个“后缀”比如: string。
然而当你真正接手一个中后台项目或者像文中这样的 TodoList 实战时你会发现 TS 的灵魂在于定义契约Contract。
这个 TodoList 项目虽然功能简单但它完美地涵盖了数据定义、状态管理、本地存储和组件通信这四大核心场景。
通过分析这段代码我们将一起揭开 TS 在实际开发中的面纱。
数据契约的基石 —— Interface 与 Type场景重现在项目中我们需要管理待办事项Todo。
在 JS 中我们直接用对象但在 TS 中我们必须先定义这个对象“长什么样”。
代码解析// 定义数据状态的接口 export interface Todo { id: number; title: string; completed: boolean; }核心干货Interface vs Type虽然两者在定义对象时非常相似但在这个场景下interface是首选。
因为interface支持声明合并Declaration Merging。
如果未来你需要扩展这个Todo比如增加一个priority字段你可以通过重新声明interface来扩展它而不需要修改原始定义。
必选与可选注意这里的属性都是必填的。
如果你的 Todo 数据中某些字段可能不存在记得加上?例如description?: string。
答疑解惑Q: 为什么一定要定义这个接口直接用any不行吗A: 看起来定义接口增加了代码量但它带来了巨大的收益智能提示IntelliSense当你写todo.的时候编辑器会立刻提示你有id、title、completed可选而不是让你盲打。
编译时检查如果你不小心写了todo.nameTS 会在编译阶段报错而不是等到运行时报undefined。
泛型Generics的魔法 —— 摆脱 any场景重现在处理localStorage时我们面临一个经典问题localStorage只能存字符串读取时需要反序列化。
如果不用泛型我们很容易写出any。
代码解析// T 类型参数 类型参数 export function getStorageT(key: string, defaultValue: T): T { const value localStorage.getItem(key); return value ? JSON.parse(value) : defaultValue; }核心干货泛型的本质这里的T就像一个占位符。
当你调用getStorageTodo[](todos, [])时TS 会自动推断出这个函数的返回值是Todo[]类型。
类型守卫通过泛型我们保证了输入什么类型就输出什么类型。
这比写死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.FCProps ({ todo, onToggle, onRemove }) { ... }核心干货函数类型的定义不要使用Function类型Function只表示“这是一个函数”但你不知道它需要什么参数。
使用(id: number) void可以精确约束函数签名。
React.FC (Function Component)虽然代码中使用了React.FC但在社区中有一种趋势是不使用React.FC。
因为React.FC默认包含了children属性如果你的组件不使用children这反而会造成类型污染。
新手建议先掌握这种写法后续再了解PropsWithChildren。
答疑解惑Q: 为什么onToggle和onRemove的参数是(id: number)而不是直接传整个todo对象A: 这是 React 性能优化的考量。
如果传整个对象子组件依赖的是对象引用。
在父组件重新渲染时如果对象是新生成的即使内容没变子组件也会强制更新。
只传id基本类型配合useCallback可以更好地控制子组件的重渲染。
Hooks 与 初始化逻辑的类型推断场景重现在自定义 HookuseTodos中我们不仅要管理状态还要处理本地存储的读取。
代码解析const [todos, setTodos] useStateTodo[]( () getStorageTodo[](STORAGE_KEY, []) );核心干货显式泛型标注虽然 TS 通常能自动推断类型但在useState的初始化函数中显式标注Todo[]是一种良好的防御性编程习惯。
这确保了setTodos的参数类型也被锁定为Todo[]。
副作用依赖useEffect的依赖项[todos]因为todos有了明确的类型TS 能防止你遗漏依赖或者错误地放入了非原始类型的依赖导致死循环。
牛刀小试
在getStorage函数中如果localStorage里的数据被篡改了比如变成了字符串而不是数组TS 能检测出来吗考察点TS 的静态类型与运行时安全的区别。
参考回答“TS 只在编译时检查无法检测运行时的数据篡改。
进阶方案在真实项目中我会结合运行时类型检查库如io-ts或zod。
在getStorage内部对JSON.parse的结果进行二次校验如果不符合Todo[]的结构就回退到默认值从而保证程序的健壮性。
”
在useTodos中为什么要写useStateTodo[]不写泛型行不行考察点类型推断的边界。
参考回答“如果不写TS 会根据默认值[]推断出类型为never[]或者根据getStorage的泛型推断。
虽然通常能推断正确但显式声明是一种**自文档化Self-documenting**的行为。
它明确告诉维护者‘这个 State 必须是 Todo 数组’。
这符合 TS 的核心原则——让错误在编码阶段暴露而不是在重构时才发现。
”
import type和普通的import有什么区别你为什么在代码里用了import type考察点ES Module 打包与类型擦除。
参考回答import type仅在编译阶段使用不会生成任何 JavaScript 代码会被‘擦除’。
使用它的主要目的是防止循环依赖和减少打包体积。
如果 A 文件 import 了 B 的类型B 又 import 了 A 的类型使用import type可以打破这种循环因为类型在运行时是不存在的。
”结语通过这个 TodoList 项目希望你能明白 TypeScript 不仅仅是“加类型”而是一种思维方式的转变。
从“我打算怎么写代码”转变为“我定义了什么规则代码必须遵守规则”。
不要害怕报错每一次 TS 的报错提示都是它在教你如何写出更健壮的代码。
继续加油