TypeScript实战:TodoList项目中的类型系统应用

作者:纸飞机时间:2026-10-26 08:49:44浏览:272

本文通过一个TypeScript实现的TodoList项目,深入解析前端开发中的核心类型概念。内容涵盖数据驱动视图的基本逻辑,重点讲解如何使用Readonly修饰符防止副作用,利用交叉类型(Intersection Types)复用接口,以及通过联合类型(Union Types)处理复杂状态。文章结合具体代码示例,展示如何在实际项目中应用TypeScript的类型校验与类型收窄技术,提升代码健壮性。

为什么用todolist

现代前端框架的教学内容早已超越了简单的Hello World示例,而是要求开发者具备数据绑定、列表遍历、条件判断等综合逻辑处理能力。TodoList因其功能完整且逻辑清晰,成为展示这些能力的理想载体,例如React和SolidJS等主流框架的官方教程均以此为例。

如果您希望查看不同前端框架对TodoList的实现方式,可以访问TodoMVC网站,该站点展示了多种框架的具体实现方案。

为什么用todolist

todolist的ts化

针对TypeScript的学习,目前官方提供的示例多为基础语法演示,缺乏结合真实项目场景的实战教程。许多开发者在掌握基础类型定义后,面对复杂项目结构时仍感到困惑。因此,本教程旨在通过具体案例填补这一实战空白。

在开始之前,需要明确本教程不依赖React、Vue等任何前端库,以保持对TypeScript核心特性的聚焦。为了精简内容,教程仅展示关键的TypeScript代码片段,无需展示完整应用即可清晰阐述TypeScript的使用技巧。

为什么用todolist

数据到视图

为什么用todolist

TodoList背后的数据结构通常表现为对象数组。以常见的视图界面为例,其底层数据应包含以下字段结构:

[
  {
    id: 1,
    text: '待办事项1',
    done: false
  },
  {
    id: 2,
    text: '待办事项2',
    done: false
  },
  {
    id: 3,
    text: '待办事项3',
    done: false
  }
]

其中,id用于标识每个待办事项的唯一性,text存储事项的具体名称,而done字段则标记该事项是否已完成。

为什么用todolist

当用户点击完成按钮时,实质上是修改了对应项的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使用的资料请关注本站其它相关文章!

作者最新文章
相关文章
更多