新手零压力学TS:从JS痛点到Todos实战
前端开发中,JS的弱类型、动态特性常带来隐患:传错参数类型、运行时才暴露bug、重构全靠猜……而TypeScript(TS)作为JS超集,添加静态类型约束,能提前规避90%低级错误,让代码更健壮。本文精简核心内容,从痛点到实战,新手快速上手。
一、为什么需要TypeScript?(JS痛点VS TS优势)
1. JavaScript核心痛点
JS是弱类型、动态语言,变量类型可随意切换,错误仅在运行时暴露,缺乏类型约束,可读性和可维护性差。
// JS加法函数,存在二义性
function add(a, b) {
return a + b;
}
// 预期求和,实际拼接,运行时才报错
const result = add(10, "5"); // 输出 "105"而非15
2. TypeScript核心优势
- 静态类型约束,提前规避类型错误
- 边写边检查bug,编辑器实时提示
- 编译时拦截错误,减少线上隐患
- 完善代码提示,提升开发效率
- 提示冗余代码,重构更安全
// TS加法函数,指定参数和返回值类型
function addTs(a: number, b: number): number {
return a + b;
}
const result3 = addTs(10, "5"); // 编写时直接报错
3. 快速安装TS
npm install -g typescript
运行 tsc -v 查看版本,出现版本号即安装成功。
二、TS基础语法(新手必学极简版)
1. 基础类型声明
let a: number = 1;
let b: string = 'test';
let c: boolean = true;
let arr: number[] = [1, 2, 3];
let arr2: Array<string> = ['a', 'b']; // 泛型写法
let tuple: [number, string] = [1, 'test']; // 元组
2. 任意类型:any与unknown
// any:新手救命稻草,放弃类型约束
let aa: any = 1;
aa = "test";
// unknown:更安全,使用前需类型检测
let bb: unknown = 1;
if (typeof bb === 'string') {
console.log(bb.length);
}
3. 枚举类型:enum
// 管理固定取值(如状态)
enum Status {
Pending, // 0
Success, // 1
Failed, // 2
}
let s: Status = Status.Success;
4. 接口与自定义类型
// 接口:约束对象结构
interface User {
id: number;
name: string;
age?: number; // 可选属性
}
// 自定义类型:灵活组合
type ID = string | number;
let num: ID = "100";
5. 泛型:类型传参
// 一套代码适配多种类型
function returnValue<T>(a: T): T {
return a;
}
const str = returnValue("test"); // T自动推导为string
三、TS实战:Todos案例(详细易懂版)
实战是掌握TS的关键,这里我们用TS实现一个完整的「待办列表」,包含「新增待办、切换完成状态、删除待办」3个核心功能,同时实现「数据持久化」(页面刷新后数据不丢失),全程讲解每一步的逻辑,新手也能看懂、会用。
先明确实战核心目标:用TS的类型约束保证代码安全,用localStorage实现数据持久化,用组件化思想拆分代码,让逻辑更清晰。
1. 封装本地存储工具(核心铺垫)
为什么要封装?原生localStorage有两个致命问题:① 只能存储字符串,存数组、对象会自动转成字符串,读取时需要手动转回来;② 没有类型约束,读取的数据是any类型,容易出错。
我们用TS+泛型封装两个工具函数,解决以上问题,同时添加异常捕获,避免数据解析失败导致页面报错。
// storage.ts(本地存储工具文件)
// 泛型<T>:保证「存什么类型,读出来就是什么类型」,避免类型丢失
export function getStorage<T>(key: string, defaultValue: T): T {
try {
// 1. 从localStorage读取指定key的值(原生读取的是字符串)
const value = localStorage.getItem(key);
// 2. 有值:用JSON.parse转成原类型(数组/对象/数字);无值:返回默认值
return value ? JSON.parse(value) : defaultValue;
} catch (e) {
// 异常捕获:如果数据格式损坏,避免页面报错,返回默认值
console.error('读取本地存储失败', e);
return defaultValue;
}
}
// 写入本地存储:自动将任意类型转成字符串
export function setStorage<T>(key: string, value: T) {
try {
localStorage.setItem(key, JSON.stringify(value));
} catch (e) {
console.error('写入本地存储失败', e);
}
}
关键讲解:
- 泛型:相当于给函数传「类型参数」,比如存的是待办数组(Todo[]),T就会被推导为Todo[],读取时返回值也一定是Todo[],全程类型安全。
- try-catch:防止本地存储的数据格式损坏(比如手动修改了localStorage的值),导致JSON.parse报错,提升代码健壮性。
- 使用场景:后面的Todos功能,新增、切换、删除待办后,都会调用这两个函数,同步数据到本地存储,实现刷新不丢失。
2. 核心逻辑封装(useTodos Hook)
我们用React的useState Hook,封装Todos的所有核心逻辑(新增、切换、删除),同时引入上面的本地存储工具,实现数据持久化。这里重点讲解TS的类型约束和逻辑拆解。
// hooks/useTodos.ts(核心逻辑文件)
import { useState } from 'react';
// 导入封装好的本地存储工具
import { getStorage, setStorage } from './storage';
// 第一步:用interface定义Todo类型,约束单个待办的数据结构
// 每个待办必须有id(唯一标识)、title(待办标题)、completed(是否完成)
interface Todo {
id: number;
title: string;
completed: boolean;
}
// 封装自定义Hook,暴露状态和方法供组件使用
export function useTodos() {
// 第二步:初始化待办列表,从本地存储读取数据,默认值是空数组
// useState的泛型<Todo[]>:约束todos是Todo类型的数组,避免存错数据
const [todos, setTodos] = useState<Todo[]>(() => getStorage('todos', []));
// 第三步:新增待办方法
const addTodo = (title: string) => {
// 1. 创建新的待办项,严格符合Todo类型约束
const newTodo: Todo = {
id: Date.now(), // 用时间戳作为唯一id,避免重复
title, // 接收用户输入的待办标题
completed: false // 新增待办默认未完成
};
// 2. 生成新的待办列表(React不可变数据原则:不直接修改原数组,创建新数组)
const newTodos = [...todos, newTodo];
// 3. 更新状态,重新渲染组件
setTodos(newTodos);
// 4. 同步新数据到本地存储,实现刷新不丢失
setStorage('todos', newTodos);
};
// 第四步:切换待办完成状态方法
const toggleTodo = (id: number) => {
// 遍历待办列表,找到当前点击的待办项,切换它的completed状态
const newTodos = todos.map(t =>
t.id === id ? { ...t, completed: !t.completed } : t
);
setTodos(newTodos);
setStorage('todos', newTodos); // 同步到本地存储
};
// 第五步:删除待办方法
const removeTodo = (id: number) => {
// 过滤掉当前要删除的待办项,生成新数组
const newTodos = todos.filter(t => t.id !== id);
setTodos(newTodos);
setStorage('todos', newTodos); // 同步到本地存储
};
// 暴露状态(todos)和方法(新增、切换、删除),供组件使用
return { todos, addTodo, toggleTodo, removeTodo };
}
关键讲解(新手必看):
- interface Todo:约束单个待办的结构,确保每个待办都有id、title、completed,且类型正确(id是数字、title是字符串),避免传错数据。
- useState<Todo[]>:约束todos是Todo类型的数组,TS会实时校验,比如往数组里push一个没有id的对象,会直接报错。
- 不可变数据原则:React中不能直接修改原状态(比如todos.push(newTodo)),必须创建新数组([...todos, newTodo]),否则组件不会重新渲染。
- 方法的类型约束:addTodo的参数title是string类型,toggleTodo和removeTodo的参数id是number类型,TS会校验调用时传入的参数是否正确。
3. 组件实现(拆分讲解,通俗易懂)
遵循React组件化思想,我们将Todos拆分成3个小组件,各司其职、逻辑清晰,同时用TS约束组件的Props(父组件传给子组件的属性),避免传错参数。
(1)TodoInput:输入框组件(负责新增待办的输入)
// components/TodoInput.tsx
import * as React from 'react';
// 定义Props类型:约束父组件必须传递onAdd方法(新增待办的方法)
interface Props {
onAdd: (title: string) => void; // 方法参数是待办标题(string),无返回值
}
// React.FC<Props>:约束这个组件是React函数组件,且接收的Props符合上面的类型
const TodoInput: React.FC<Props> = ({ onAdd }) => {
// 管理输入框的输入内容,类型约束为string
const [value, setValue] = React.useState<string>('');
// 点击「添加」按钮的处理逻辑
const handleAdd = () => {
// 校验:输入为空或纯空格,不执行新增(避免无效待办)
if (!value.trim()) return;
// 调用父组件传递的onAdd方法,传入输入的待办标题
onAdd(value);
// 清空输入框,提升用户体验
setValue('');
};
return (
<div>
<input
value={value}
onChange={e => setValue(e.target.value)} // 输入变化时,更新输入框状态
placeholder="输入待办标题"
onKeyDown={e => e.key === 'Enter' && handleAdd()} // 支持按回车新增
/>
<button onClick={handleAdd}>添加待办</button>
</div>
);
};
export default TodoInput;
(2)TodoItem:单个待办项组件(负责渲染单个待办,绑定交互)
// components/TodoItem.tsx
import * as React from 'react';
// 导入Todo类型,约束单个待办的数据结构
import type { Todo } from '../hooks/useTodos';
// 定义Props类型:父组件需要传递单个待办、切换状态方法、删除方法
interface Props {
todo: Todo; // 单个待办,符合Todo类型约束
onToggle: (id: number) => void; // 切换完成状态的方法
onRemove: (id: number) => void; // 删除待办的方法
}
const TodoItem: React.FC<Props> = ({ todo, onToggle, onRemove }) => {
return (
<li>
{/* 复选框:勾选状态绑定todo.completed,点击切换状态 */}
<input
type="checkbox"
checked={todo.completed}
onChange={() => onToggle(todo.id)}
/>
{/* 待办标题:完成状态显示删除线 */}
<span style={{ textDecoration: todo.completed ? 'line-through' : 'none' }}>
{todo.title}
</span>
{/* 删除按钮:点击删除当前待办 */}
<button onClick={() => onRemove(todo.id)}>删除</button>
</li>
);
};
export default TodoItem;
(3)TodoList:待办列表容器(负责循环渲染所有待办项)
// components/TodoList.tsx
import * as React from 'react';
import type { Todo } from '../hooks/useTodos';
import TodoItem from './TodoItem'; // 导入单个待办组件
// 定义Props类型:接收待办列表、切换方法、删除方法
interface Props {
todos: Todo[]; // 待办列表(Todo类型的数组)
onToggle: (id: number) => void;
onRemove: (id: number) => void;
}
const TodoList: React.FC<Props> = ({ todos, onToggle, onRemove }) => {
return (
<ul>
{/* 循环遍历待办列表,每个待办项渲染一个TodoItem组件 */}
{todos.map((todo) => (
<TodoItem
key={todo.id} // React列表渲染必须的唯一key(用待办id,避免重复)
todo={todo} // 给单个待办组件传递当前待办数据
onToggle={onToggle} // 传递切换状态方法
onRemove={onRemove} // 传递删除方法
/>
))}
</ul>
);
};
export default TodoList;
(4)根组件App:组合所有组件,实现完整功能
// App.tsx
import * as React from 'react';
import { useTodos } from './hooks/useTodos'; // 导入核心逻辑
import TodoInput from './components/TodoInput'; // 导入输入框组件
import TodoList from './components/TodoList'; // 导入列表组件
function App() {
// 从useTodos Hook中获取待办列表和方法
const { todos, addTodo, toggleTodo, removeTodo } = useTodos();
return (
<div>
<h1>TS实战:待办列表</h1>
{/* 传递addTodo方法给输入框组件,用于新增待办 */}
<TodoInput onAdd={addTodo} />
{/* 传递待办列表和方法给列表组件,用于渲染和交互 */}
{todos.length === 0 ? (
<p>暂无待办,添加你的第一个待办吧!</p>
) : (
<TodoList todos={todos} onToggle={toggleTodo} onRemove={removeTodo} />
)}
</div>
);
}
export default App;
组件交互链路(新手必懂):
- 用户在TodoInput输入框输入待办标题,点击「添加」或按回车;
- 触发TodoInput的handleAdd方法,调用父组件(App)传递的addTodo方法;
- addTodo方法创建新待办、更新todos状态,并同步到本地存储;
- todos状态更新后,App组件重新渲染,TodoList接收到新的todos数组,循环渲染TodoItem;
- 点击TodoItem的复选框,触发toggleTodo方法,切换待办完成状态;点击删除按钮,触发removeTodo方法,删除待办,且都会同步到本地存储。
拆分TodoInput(输入框)、TodoItem(单个待办)、TodoList(列表),通过Props传递数据,实现新增、切换、删除功能,数据持久化到localStorage,全程类型约束。
四、新手避坑总结
- TS是JS超集,可逐步过渡,不用一次性掌握所有语法。
- any尽量少用,优先用unknown,避免失去类型约束意义。
- 核心是类型约束,实战比死记语法更有效。