本文通过一个TypeScript实现的TodoList项目,深入解析前端开发中的核心类型概念。内容涵盖数据驱动视图的基本逻辑,重点讲解如何使用Readonly修饰符防止副作用,利用交叉类型(Intersection Types)复用接口,以及通过联合类型(Union Types)处理复杂状态。文章结合具体代码示例,展示如何在实际项目中应用TypeScript的类型校验与类型收窄技术,提升代码健壮性。
为什么用todolist
现代前端框架的教学内容早已超越了简单的Hello World示例,而是要求开发者具备数据绑定、列表遍历、条件判断等综合逻辑处理能力。TodoList因其功能完整且逻辑清晰,成为展示这些能力的理想载体,例如React和SolidJS等主流框架的官方教程均以此为例。
如果您希望查看不同前端框架对TodoList的实现方式,可以访问TodoMVC网站,该站点展示了多种框架的具体实现方案。

todolist的ts化
针对TypeScript的学习,目前官方提供的示例多为基础语法演示,缺乏结合真实项目场景的实战教程。许多开发者在掌握基础类型定义后,面对复杂项目结构时仍感到困惑。因此,本教程旨在通过具体案例填补这一实战空白。
在开始之前,需要明确本教程不依赖React、Vue等任何前端库,以保持对TypeScript核心特性的聚焦。为了精简内容,教程仅展示关键的TypeScript代码片段,无需展示完整应用即可清晰阐述TypeScript的使用技巧。

数据到视图

TodoList背后的数据结构通常表现为对象数组。以常见的视图界面为例,其底层数据应包含以下字段结构:
[
{
id: 1,
text: '待办事项1',
done: false
},
{
id: 2,
text: '待办事项2',
done: false
},
{
id: 3,
text: '待办事项3',
done: false
}
]
其中,id用于标识每个待办事项的唯一性,text存储事项的具体名称,而done字段则标记该事项是否已完成。

当用户点击完成按钮时,实质上是修改了对应项的done字段值。数据的变更会驱动视图进行相应的更新,实现数据到视图的同步。
实现handleTodoItem
针对上述点击事件,我们先使用JavaScript编写伪代码逻辑:点击时需处理数据,具体表现为返回一个新的对象,其中done状态取反。
function handleTodoItem(todo){
// 点击的时候todo中的done的布尔值取反
return {
...todo,
done: !todo.done
}
}
接下来,我们使用TypeScript对该逻辑进行类型优化,确保代码的类型安全性。
type Todo = {
id: number;
text: string;
done: boolean;
}
// 如果某个变量是todo类型,可以这样
const todoItem: Todo = {
id: 1,
text: '待办事项1',
done: false
}
通过TypeScript定义类型后,如果传入的todoItem不匹配预期类型,编译器将直接报错。这种机制允许开发者在编译阶段提前感知错误,若项目配置了TypeScript环境,VSCode等编辑器会即时提示相关错误信息。
那么,在TypeScript环境下,handleTodoItem方法应如何定义呢?
function handleTodoItem(todo: Todo): Todo {
// 逻辑实现
}
readonly
对于handleTodoItem函数而言,它应当是无副作用的纯函数。这意味着传入的todo对象不应被直接修改,而应返回一个包含新状态的新对象。
例如,直接修改传入参数的done属性并返回原对象的做法虽然能实现功能,但引入了副作用,改变了原始数据,这种写法在TypeScript中是不被推荐的。
function handleTodoItem(todo: Todo):Todo {
// 点击的时候todo中的done的布尔值取反
todo.done = !todo.done
return todo
}
然而,仅靠上述写法TypeScript并不会报错。为了强制约束,我们可以利用TypeScript的类型系统进行校验,具体做法如下:
type Todo = {
readonly id: number;
readonly text: string;
readonly done: boolean;
}
此时,若尝试直接修改对象属性,TypeScript会抛出错误,因为Todo类型被定义为只读(readonly)。此外,也可以使用Readonly工具类型将对象的所有属性统一设置为只读。
type Todo = Readonly<{
id: number;
text: string;
done: boolean;
}>
在TypeScript中,除了Readonly,还有Required、Partial等丰富的工具类型。如有需要,读者可自行查阅官方文档了解更多用法。
分类
对于已完成的列表,我们需要进行筛选分类。例如,筛选出所有已完成的项目,其结果仍为一个数组,且每个元素的done字段值为true。
[
{
id: 1,
text: '待办事项1',
done: true
},
{
id: 2,
text: '待办事项2',
done: true
}
]
如何表示这种数组类型的数据呢?使用Todo[]这种语法即可表示Todo类型的数组。同样,为了避免副作用,函数参数也应设为只读,因此函数签名可定义为:
function completeTodoList(
todos: readonly Todo[]
): Todo[] {
// ...
}
此外,由于Todo类型中的done字段为boolean,而在completeTodoList函数中,筛选出的元素done值固定为true,因此我们需要重新定义一个专门表示已完成事项的类型。
type CompletedTodo = Readonly<{
id: number;
text: string;
done: true;
}>
基于上述定义,completeTodoList方法的实现应调整为:
function completeTodoList(
todos: readonly Todo[]
): CompletedTodo[] {
// ...
}
交叉类型
观察Todo和CompletedTodo这两个类型,发现它们的id和text字段是重复的。为了消除冗余代码,我们可以使用交叉类型(Intersection Types)来合并逻辑。
举个例子,当两个类型通过交叉运算符连接时:
type A = {a: number}
type B = {b: string}
type AandB = A & B
// 结果为
// {
// a: number
// b: string
// }
如果两个类型包含相同的key,后一个类型的key定义会覆盖前一个的定义。
type A = {key: number}
type B = {key: string}
type AandB = A & B
// 结果为
// {
// key: string
// }
那么,针对Todo和CompletedTodo,如何从Todo通过交叉类型推导出CompletedTodo呢?
type CompletedTodo = Todo & {
readonly done: true
}
这种写法非常简洁,同时有效去除了重复的类型定义代码。
新增功能
如果在Todo的基础上新增优先级功能,可以使用priority字段来表示。
优先级共有三种预设等级。
!!!
!!
!
你可以使用priority: 2这样设置,界面展示为【!!】。当然,你也可以自定义优先级,例如:
{
priority: {
custom: '紧急'
}
}
此时,数据结构变为包含不同优先级形式的数组。
[
{
id: 1,
text: '待办事项1',
done: false,
priority: 1
},
{
id: 2,
text: '待办事项2',
done: false,
priority: 2
},
{
id: 3,
text: '待办事项3',
done: false,
priority: {
custom: '紧急'
}
},
{
id: 4,
text: '待办事项4',
done: false
},
]
在已有Todo类型的基础上,如何新增一个key呢?
联合类型
之前我们使用&连接类型得到交叉类型,而联合类型则是通过|连接。举个例子:
type Foo = number | string;
这表示Foo类型可以是number或string。因此,我们的priority类型可以定义为:
type Priority = 1 | 2 | 3 | { custom: string };
此时priority即为我们期望的内容,Todo的类型定义也随之更新。
type Todo = Readonly<{
id: number;
text: string;
done: boolean;
priority: Priority;
}>
可选属性
上述priority属性目前是必填的,但在实际场景中,用户可能不设置优先级。针对这种情况,我们可以使用可选属性。
type Todo = Readonly<{
id: number;
text: string;
done: boolean;
priority?: Priority;
}>
在对应的属性名后添加?符号即可将其标记为可选。
数据转视图
既然有了priority数据,如何将1、2、3等数值或自定义对象转换为【!!】等展示形式呢?
我们可以自定义一个函数priorityToString来完成这一转换。
priorityToString(1)
// !
priorityToString(2)
// !!
priorityToString(3)
// !!!
priorityToString({ custom: '紧急' })
// 紧急
如果情况较少,你可能会这样编写函数:
function priorityToString(priority: Priority): string {
if(priority === 1){
return '!'
}else if(priority === 2){
return '!!'
}else if(priority === 3){
return '!!!'
}else{
return priority.custom
}
}
在使用联合类型进行if条件判断时,TypeScript会自动进行类型收窄(Type Narrowing),确认每个分支下的参数具体类型。这正是联合类型的强大之处。
总结
本文概括了项目中常用的TypeScript知识点,通过一个简单的TodoList项目,对TypeScript的基本类型、工具类型及高级特性进行了简要说明。
参考:ts.chibicode.com/todo/
以上就是项目中使用TypeScript的TodoList实例详解的详细内容,更多关于TypeScript TodoList使用的资料请关注本站其它相关文章!