TypeScript 基础
环境配置命令:
bash
# 克隆课程仓库
git clone <repo-address>
cd ZTM-TypeScript
# 安装依赖
pnpm install
# 运行TypeScript文件
pnpm exec ts-node demo/<folder>/<file>.ts
# 查看解决方案对比
git fetch origin solutions
# 在VS Code中右键文件 → "Open Changes with Branch Tag" → 选择solutions分支
推荐VS Code插件:
- GitLens:Git历史追踪
- Error Lens:行内错误高亮
2. TypeScript vs JavaScript
2.1 类型安全减少样板代码
JavaScript的问题示例:
javascript
// 需要23行防御性代码检查参数类型和默认值
function popup(message, options) {
// 5行设置默认值
const defaultOptions = { kind: "info", timeout: 5000 };
options = { ...defaultOptions, ...options };
// 18行参数验证(boilerplate)
if (typeof message !== "string") throw new Error("Invalid message");
if (options.timeout < 0) throw new Error("Invalid timeout");
// ... 更多验证逻辑
// 实际功能代码仅5行
}
TypeScript的解决方案:
typescript
type PopupOptions = {
kind: "info" | "error" | "warning";
timeout: number; // 明确为毫秒
};
function popup(message: string, options: PopupOptions): void {
// 零行防御代码,类型系统在编译时强制约束
// 直接编写业务逻辑...
}
对比:在上述示例中,TypeScript版本减少了**38%**的代码量,且无需运行时类型检查测试用例。
2.2 IDE智能提示(IntelliSense)
由于类型信息编码在类型系统中,编辑器可提供:
- 自动补全可用字段(如
options.kind的合法值) - 实时错误检测(输入时即报错,非运行时)
- 自动重构支持
3. 基础语法与类型系统
3.1 变量声明
关键字选择原则:
const:默认使用。阻止变量重新赋值(但允许修改对象/数组内部数据)。let:仅在需要重新赋值时使用(如计数器、累加器)。var:永远避免使用。遗留关键字,具有函数作用域和变量提升等非常规行为。
typescript
// 常量声明(推荐默认)
const courseName: string = "TypeScript";
const isActive: boolean = true;
// 允许修改(内部数据可变)
const user = { name: "Alice", age: 30 };
user.age = 31; // 合法
// user = { name: "Bob" }; // 非法:不能重新赋值整个对象
// 可变变量
let count: number = 0;
count = 1; // 合法
// 变量遮蔽(Variable Shadowing)
let value = 10;
{
let value = 20; // 内部作用域遮蔽外部变量
console.log(value); // 20
}
console.log(value); // 10
3.2 基础类型
| 类型 | 示例 | 说明 |
|---|---|---|
string |
"hello", 'world', template |
单双引号无区别,反引号支持模板字符串 |
number |
42, 3.14, 1e3, 0xFF, 0b1010 |
包含整数、浮点数、科学计数法、十六进制、二进制 |
bigint |
9000n |
任意精度整数,后缀加 n |
boolean |
true, false |
逻辑真/假 |
null |
null |
明确表示"无值"(程序员主动设置) |
undefined |
undefined |
表示"未定义"(变量存在但未赋值) |
未初始化变量:
typescript
let greeting: string; // 声明但未初始化
// console.log(greeting); // TS错误:使用前未赋值
if (condition) {
greeting = "hi";
} else {
greeting = "hey";
}
// 此时使用greeting安全,因为所有分支都赋值了
3.3 运算符
严格相等 :TypeScript/JavaScript中始终使用 === 和 !==,避免使用 ==/!=(会进行隐式类型转换)。
typescript
// 比较运算符
const age = 18;
const canPurchase: boolean = age >= 18; // true
const isAdult: boolean = age === 18; // true
const isNotChild: boolean = age !== 12; // true
// 逻辑运算符
const hasSkill: boolean = true;
const isTuesday: boolean = false;
const canWork: boolean = hasSkill && isTuesday; // AND
const isAvailable: boolean = hasSkill || isTuesday; // OR
const isBusy: boolean = !isAvailable; // NOT(取反)
// 短路求值(Short-circuit)
// &&:第一个为false则停止,返回false
// ||:第一个为true则停止,返回true
算术与赋值运算符:
typescript
let n = 5;
// 递增/递减(避免与赋值混用)
n++; // 后缀递增(返回原值后加1)
++n; // 前缀递增(加1后返回新值)
// 推荐:算术赋值运算符(更清晰)
n += 5; // 加
n -= 3; // 减
n *= 2; // 乘
n /= 4; // 除(注意除零得Infinity或NaN)
n %= 3; // 取模(余数)
n **= 2; // 指数(ES2016+)
// Math模块
const pi: number = Math.PI;
const absValue: number = Math.abs(-5); // 5
4. 函数与类型注解
4.1 函数基础
函数声明与调用:
typescript
// 无参数、无返回值(void)
function sayHello(): void {
console.log("Hello");
}
sayHello();
// 带参数、带返回值(必须类型注解)
function sum(lhs: number, rhs: number): number {
return lhs + rhs;
}
// 函数调用与嵌套
const result: number = sum(sum(1, 2), sum(3, 4)); // 10
// 分解嵌套以提高可读性
const left: number = sum(1, 2);
const right: number = sum(3, 4);
const final: number = sum(left, right);
4.2 函数表达式与箭头函数
typescript
// 函数表达式(赋值给变量)
const add = function(a: number, b: number): number {
return a + b;
};
// 箭头函数(推荐语法)
const arrowSum = (lhs: number, rhs: number): number => {
return lhs + rhs;
};
// 简写形式(单行表达式)
const multiply = (a: number, b: number): number => a * b;
高阶函数(函数作为参数):
typescript
type CalculationFn = (lhs: number, rhs: number) => number;
function calculate(
fn: CalculationFn,
lhs: number,
rhs: number
): number {
const result = fn(lhs, rhs);
// 可添加额外逻辑(如日志、验证)
return result;
}
// 使用
calculate((a, b) => a + b, 5, 6); // 11
calculate((a, b) => a - b, 10, 3); // 7
4.3 类型注解的最佳实践
黄金法则 :始终为函数参数和返回值添加类型注解
typescript
// 正确:完整类型注解
function greet(name: string): string {
return `Hello, ${name}`;
}
// 类型推断的局限(以下情况需显式注解)
const numbers: number[] = [1, 2, 3];
// numbers.push("4"); // TS会阻止这种错误
// 模板字符串(Template Literals)
const user = "TypeScript";
const message: string = `Welcome, ${user}!`; // 使用反引号(backticks)
// 多行模板与表达式
const total = 6;
const report: string = `There are ${2 + 4} people`; // 可嵌入表达式
避免过度注解:
typescript
// 冗余(TS能推断)
const count: number = 5;
const double: number = count * 2;
// 推荐(利用类型推断)
const count = 5; // TS自动推断为number
const double = count * 2; // 推断为number
// 但函数必须注解!
function doubleValue(x: number): number {
return x * 2;
}
5. 控制流
5.1 条件语句
If/Else/Else If:
typescript
const age = 18;
if (age < 13) {
console.log("Child");
} else if (age < 20) {
console.log("Teenager"); // 执行这里
} else {
console.log("Adult");
}
// 简化复杂条件:使用早期返回(Early Returns)
function approveWork(
hasSkills: boolean,
hoursWorked: number,
totalOvertime: number,
isHoliday: boolean,
day: string
): string {
// 过滤器模式:逐个检查,失败立即返回
if (!hasSkills) return "Go home";
if (hoursWorked <= 8 || totalOvertime >= 1) return "Go home";
if (!isHoliday && day !== "Tuesday") return "Go home";
return "Approved"; // 通过所有检查
}
Switch语句:
typescript
const command: string = "add";
switch (command) {
case "add":
console.log("Adding...");
break; // 必须显式break,否则fall-through
case "remove":
console.log("Removing...");
break;
case "list":
case "show": // 多个case共享代码(fall-through故意使用)
console.log("Listing...");
break;
default:
console.log("Unknown command");
}
// 等效于if/else,但更清晰处理多值相等判断
三元运算符(仅用于简单赋值):
typescript
const age = 16;
const status: string = age >= 18 ? "Adult" : "Minor";
// 避免嵌套三元(可读性差)
// const result = condition1 ? (condition2 ? a : b) : (condition3 ? c : d);
5.2 循环结构
For循环(推荐用于已知次数):
typescript
// 标准for循环(C风格)
for (let i = 0; i < 5; i++) {
console.log(i); // 0,1,2,3,4
}
// 倒序
for (let i = 5; i > 0; i--) {
console.log(i); // 5,4,3,2,1
}
// 遍历数组(传统方式)
const letters = ["a", "b", "c"];
for (let i = 0; i < letters.length; i++) {
console.log(letters[i]);
}
// 控制关键字
for (let i = 0; i < 100; i++) {
if (i === 3) break; // 完全退出循环
if (i % 2 === 0) continue; // 跳过当前迭代,进入下一次
console.log(i); // 1, 5, 7, 9...(跳过偶数,3时停止)
}
While循环(推荐用于条件不确定):
typescript
let i = 0;
while (i < 5) {
console.log(i);
i++; // 手动更新条件变量,避免死循环
}
// 无限循环(需确保有break)
while (true) {
const input = getInput();
if (input === "quit") break;
process(input);
}
6. 数据结构与类型
6.1 类型别名(Type Aliases)
为现有类型创建语义化名称,提高代码自文档性
typescript
// 基础类型别名
type PersonName = string;
type Year = number;
// 对象类型别名(核心)
type Coordinate = {
x: number;
y: number;
};
type Location = {
coord: Coordinate;
name: PersonName;
};
// 使用
const origin: Coordinate = { x: 0, y: 0 };
const home: Location = {
coord: { x: 10, y: 20 },
name: "Home"
};
// 访问嵌套属性
const xPos: number = home.coord.x;
// 函数签名更清晰
function printName(name: PersonName): void {
console.log(name);
}
// 比 function printName(name: string): void 更具语义
6.2 数组(Arrays)
存储同类型数据的集合。
typescript
// 声明与初始化
const numbers: number[] = [1, 2, 3];
const names: Array<string> = ["Alice", "Bob"]; // 泛型语法(等价)
// 空数组(需类型注解)
const empty: string[] = [];
// 多维数组
const matrix: number[][] = [
[1, 2],
[3, 4]
];
const value = matrix[0][1]; // 2
// 常用操作(mutable)
const arr = [10, 20, 30];
arr.push(40); // 末尾添加 [10,20,30,40]
arr.pop(); // 移除末尾 [10,20,30]
arr.splice(1, 1); // 从索引1移除1个元素 [10,30]
// 对象数组
type Link = { title: string; url: string };
const links: Link[] = [
{ title: "MS", url: "https://microsoft.com" },
{ title: "TS", url: "https://typescriptlang.org" }
];
// 访问对象数组元素
const tsUrl: string = links[1].url;
6.3 元组(Tuples)
固定长度、固定类型的有序集合。
typescript
// 定义:指定每个位置的类型
type Book = [string, number]; // [标题, 年份]
// 使用(必须显式注解以区分数组)
const myBook: Book = ["Sample", 1980];
// myBook[0] 推断为 string
// myBook[1] 推断为 number
// 解构(推荐用法)
const [title, year] = myBook;
console.log(title); // "Sample"
console.log(year); // 1980
// 函数返回多值
function getCoordinates(): [number, number] {
return [3, 5];
}
const [x, y] = getCoordinates();
// 元组数组
const points: [number, number][] = [
[0, 0],
[1, 1],
[2, 2]
];
7. 模块化编程
将代码组织为可复用的独立单元
typescript
// math.ts(模块文件)
export type Int = number;
export const PI = Math.PI;
export function add(lhs: number, rhs: number): number {
return lhs + rhs;
}
// 默认导出(不推荐用于新项目,但需能识别他人代码)
export default type Point = { x: number; y: number };
// core.ts
export type Point = { x: number; y: number };
// main.ts(入口文件)
import { add, PI } from "./math"; // 命名导入
import { Point } from "./core"; // 类型导入
import type { Int } from "./math"; // 类型专用导入(TS 3.8+)
// 重命名导入
import { Int as Integer } from "./math";
// 默认导入(可任意命名,但不推荐)
import MyPoint from "./math"; // 实际导入的是Point,但可重命名
// 使用
const result: number = add(2, 3);
模块设计原则:
- 单一职责:每个模块只做一件事(如数据验证、体积计算、折扣应用)
- 避免默认导出(
export default):强制使用命名导入可提高代码可读性和重构安全性
8. 项目
8.1 文本搜索工具(Grep)
功能:读取文件,按行搜索字符串。
typescript
// grep.ts
import { readFileSync } from "fs";
const args = process.argv.slice(2); // 去除前两个系统参数
const [fileName, searchString] = args;
const fileContents: string = readFileSync(fileName, "utf-8");
const lines: string[] = fileContents.split("\n");
for (let i = 0; i < lines.length; i++) {
if (lines[i].includes(searchString)) {
console.log(lines[i]);
}
}
运行命令:
bash
pnpm exec ts-node grep.ts package.json "version"
8.2 待办清单(Todo List CLI)
功能:命令行任务管理(增删查)。
核心类型与函数:
typescript
import * as fs from "fs";
type Todo = {
id: number;
task: string;
};
const TODOS_PATH = "todos.json";
// CRUD操作
function getTodos(): Todo[] {
if (!fs.existsSync(TODOS_PATH)) return [];
const data = fs.readFileSync(TODOS_PATH, "utf-8");
return JSON.parse(data) as Todo[]; // 类型断言
}
function saveTodos(todos: Todo[]): void {
fs.writeFileSync(TODOS_PATH, JSON.stringify(todos));
}
function addTodo(task: string): void {
const todos = getTodos();
const id = todos.length > 0 ? todos[todos.length - 1].id + 1 : 1;
todos.push({ id, task });
saveTodos(todos);
console.log(`Added: ID ${id} - ${task}`);
}
function removeTodo(id: number): void {
const todos = getTodos();
const index = todos.findIndex(todo => todo.id === id);
if (index === -1) {
console.log(`Todo ${id} not found`);
return;
}
const [removed] = todos.splice(index, 1);
saveTodos(todos);
console.log(`Removed: ${removed.task}`);
}
function listTodos(): void {
const todos = getTodos();
todos.forEach(todo => {
console.log(`${todo.id}: ${todo.task}`);
});
}
// CLI接口
function cli(): void {
const [, , command, ...options] = process.argv;
switch (command) {
case "add":
if (options.length !== 1) {
console.error("Usage: add <task>");
return;
}
addTodo(options[0]);
break;
case "done":
if (options.length !== 1 || isNaN(parseInt(options[0]))) {
console.error("Usage: done <id>");
return;
}
removeTodo(parseInt(options[0]));
break;
case "list":
listTodos();
break;
default:
console.log("Commands: add <task>, done <id>, list");
}
}
cli();
使用示例:
bash
pnpm exec ts-node todo.ts add "Buy groceries"
pnpm exec ts-node todo.ts add "Walk dog"
pnpm exec ts-node todo.ts list
# 输出:
# 1: Buy groceries
# 2: Walk dog
pnpm exec ts-node todo.ts done 1
pnpm exec ts-node todo.ts list
# 输出:
# 2: Walk dog
9. 总结
- 类型注解优先 :始终为函数参数和返回值添加类型,这是TS提供80%价值的地方
- const优先 :默认使用
const,必要时使用let,永远不用var - 严格相等 :始终使用
===和!== 早期返回:用早期返回替代深层嵌套的if/else单一职责:函数和模块都应专注于单一任务- 避免默认导出:使用命名导出提高代码可维护性
- 利用类型推断:简单变量无需冗余注解,让TS自动推断
后续:掌握基础后,应学习接口(Interfaces)、泛型(Generics)、类(Classes)和高级类型操作(Mapped Types、Conditional Types)构建企业级应用
React/TypeScript
掌握 React/TypeScript 开发中的实际使用场景:
- 状态管理 :
useState、useEffect、useCallback、useMemo - 组件架构:Props 传递、组件组合、条件渲染
- 数据流:Context API 全局状态、自定义 Hooks 逻辑复用
- 性能优化 :避免无限渲染、
缓存计算值与函数
1. State Management - useState
useState 是 React 最基础的状态管理 Hook,返回一个状态变量和一个更新函数。React 通过追踪这个状态来决定何时重新渲染组件
typescript
const [count, setCount] = useState<number>(0);
count:当前状态值setCount:更新状态的函数,调用后会触发组件重新渲染0:初始值
用法
错误方式(直接修改变量):
typescript
let count = 0; // 普通变量
const increment = () => {
count = count + 1; // 修改变量,但 React 无法检测
};
结果:点击按钮时控制台显示数值变化,但 UI 不更新,因为 React 没有追踪这个普通变量。
正确方式(使用 useState):
typescript
const [count, setCount] = useState(0);
const increment = () => {
setCount(count + 1); // React 检测状态变化,触发重渲染
};
2. useEffect - 处理副作用与避免无限循环
useEffect 用于处理副作用 (Side Effects):数据获取、订阅、手动修改 DOM、定时器等。绝不要在渲染函数中直接运行副作用,这会导致无限循环。
避免
** 错误示范(直接在组件内设置状态)**:
typescript
const Clock = () => {
const [time, setTime] = useState<Date | null>(null);
// 直接在渲染逻辑中设置状态 → 触发重新渲染 → 再次设置状态 → 无限循环
setTime(new Date());
return <div>{time?.toLocaleTimeString()}</div>;
};
现象:CPU 占用飙升,页面卡死,控制台疯狂输出渲染日志。
正确方式(使用 useEffect):
typescript
useEffect(() => {
// 只在组件挂载时执行
setTime(new Date());
const timer = setInterval(() => {
setTime(new Date());
}, 1000);
// 清理函数:防止内存泄漏
return () => clearInterval(timer);
}, []); // 空依赖数组 = 只运行一次
依赖数组详解
[]:仅在组件挂载和卸载时执行(适合初始化数据、设置订阅)[dependency]:当依赖项变化时重新执行(适合响应特定状态变化)无依赖数组:每次渲染后都执行(极少使用,容易导致性能问题)
3. Props 与组件组合 (Component Composition)
TypeScript Interface 定义
使用 TypeScript 时必须定义 Props 的类型接口:
typescript
interface ButtonProps {
variant: 'primary' | 'secondary' | 'destructive' | 'sean'; // 字面量联合类型
children: React.ReactNode; // 子元素
onClick?: () => void; // 可选的点击回调
disabled?: boolean; // 可选的禁用状态
type?: 'button' | 'submit' | 'reset';
}
组件复用模式
反模式(为每种样式写单独组件):
typescript
const PrimaryButton = () => <button className="primary">...</button>;
const SecondaryButton = () => <button className="secondary">...</button>;
// 重复代码,难以维护
最佳实践(通过 Props 控制变体):
typescript
const Button = ({ variant, children, onClick, disabled = false }: ButtonProps) => {
const className = `btn-${variant}`; // 动态类名
return (
<button
className={className}
onClick={onClick}
disabled={disabled}
>
{children}
</button>
);
};
// 使用
<Button variant="primary" onClick={() => alert('Primary')}>Primary</Button>
<Button variant="destructive" onClick={() => alert('Danger')}>Danger</Button>
扩展
可随时添加新的变体类型(如 sean),无需修改组件逻辑,只需在 Interface 中添加允许的值:
typescript
// 在 variant 类型中添加 'sean'
variant: 'primary' | 'secondary' | 'destructive' | 'sean';
4. 条件渲染 (Conditional Rendering)
处理异步数据状态
实际开发中必须处理三种 UI 状态:加载中 、错误 、成功。
typescript
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [user, setUser] = useState<User | null>(null);
useEffect(() => {
setTimeout(() => {
const random = Math.random();
if (random > 0.7) {
setError('Failed to fetch user');
setLoading(false);
} else {
setUser({ name: 'Sean', email: 'sean@example.com' });
setLoading(false);
}
}, 2000); // 模拟 2 秒网络延迟
}, []);
条件渲染逻辑
typescript
return (
<div>
{loading && <div>Loading user data...</div>}
{error && (
<div>
<p>Error: {error}</p>
<button onClick={fetchUser}>Try Again</button>
</div>
)}
{!loading && !error && user && (
<div>
<h3>{user.name}</h3>
<p>{user.email}</p>
</div>
)}
{!loading && !error && !user && <div>Please log in</div>}
</div>
);
用户体验:始终显示加载状态,避免用户面对空白屏幕产生困惑
5. 列表渲染与 Keys (List Rendering & Keys)
typescript
interface Todo {
id: number;
text: string;
completed: boolean;
}
const [todos, setTodos] = useState<Todo[]>([
{ id: 1, text: 'Learn React', completed: true },
{ id: 2, text: 'Master useEffect', completed: false },
// ...
]);
切换逻辑实现
typescript
const toggleTodo = (id: number) => {
setTodos(todos.map(todo =>
todo.id === id
? { ...todo, completed: !todo.completed } // 展开运算符保留其他属性
: todo
));
};
Keys 的重要性
错误方式(使用索引或重复 key):
typescript
{todos.map((todo, index) => (
<div key={index}>...</div> // 不推荐:列表重排时性能差
))}
// 或更糟:所有项使用相同 key
<div key="same-key-for-all">...</div>
正确方式(使用唯一 ID):
typescript
{todos.map(todo => (
<div
key={todo.id} // 使用数据本身的唯一标识
onClick={() => toggleTodo(todo.id)}
className={todo.completed ? 'todo-completed' : ''}
>
{todo.text}
</div>
))}
原理:React 使用 key 来识别哪些元素改变了、添加了或删除了。错误的 key 会导致状态混乱(如勾选框状态错位)
进度计算
typescript
const completedCount = todos.filter(t => t.completed).length;
const progress = (completedCount / todos.length) * 100;
6. 事件处理与表单优化 (useCallback & useMemo)
useCallback - 缓存函数引用
问题场景:每次渲染创建新函数引用,导致子组件不必要的重渲染。
解决方案:
typescript
const handleChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
const { name, value } = e.target;
setFormData(prev => ({
...prev,
[name]: value
}));
setErrors(prev => ({ ...prev, [name]: '' }));
}, []); // 空依赖:函数定义只创建一次
useMemo - 缓存计算值
用途:避免昂贵的计算在每次渲染时重复执行。
typescript
const stats = useMemo(() => {
const total = submittedData.length;
const uniqueEmails = new Set(submittedData.map(d => d.email)).size;
const avgLength = submittedData.reduce((sum, d) =>
sum + d.message.length, 0
) / (total || 1);
return { total, uniqueEmails, avgLength };
}, [submittedData]); // 只有 submittedData 变化时才重新计算
表单基础结构
typescript
const handleSubmit = useCallback((e: React.FormEvent) => {
e.preventDefault(); // 阻止默认提交行为
const newSubmission = {
id: nextId,
...formData,
timestamp: new Date().toISOString()
};
setSubmittedData(prev => [...prev, newSubmission]);
setNextId(prev => prev + 1);
setFormData({ name: '', email: '', message: '' }); // 重置表单
}, [formData, nextId]);
7. Context API - 全局状态管理
创建 Context
typescript
// contexts/theme-context.tsx
interface ThemeContextType {
theme: 'light' | 'dark';
toggleTheme: () => void;
}
const ThemeContext = createContext<ThemeContextType | undefined>(undefined);
export const ThemeProvider = ({ children }: { children: React.ReactNode }) => {
const [theme, setTheme] = useState<'light' | 'dark'>('light');
const toggleTheme = useCallback(() => {
setTheme(prev => prev === 'light' ? 'dark' : 'light');
}, []);
return (
<ThemeContext.Provider value={{ theme, toggleTheme }}>
{children}
</ThemeContext.Provider>
);
};
export const useTheme = () => {
const context = useContext(ThemeContext);
if (!context) throw new Error('useTheme must be used within ThemeProvider');
return context;
};
应用层级包裹
在布局文件中包裹(推荐):
typescript
// app/layout.tsx
export default function RootLayout({ children }) {
return (
<html>
<body>
<ThemeProvider>
{children}
</ThemeProvider>
</body>
</html>
);
}
消费 Context
typescript
const { theme, toggleTheme } = useTheme();
return (
<button onClick={toggleTheme}>
Current: {theme}
</button>
);
8. 自定义 Hooks - 逻辑复用
封装 LocalStorage 逻辑
typescript
// hooks/use-local-storage.ts
export const useLocalStorage = <T,>(key: string, initialValue: T) => {
const [storedValue, setStoredValue] = useState<T>(() => {
if (typeof window === 'undefined') return initialValue;
try {
const item = window.localStorage.getItem(key);
return item ? JSON.parse(item) : initialValue;
} catch (error) {
console.error(error);
return initialValue;
}
});
const setValue = useCallback((value: T | ((val: T) => T)) => {
try {
const valueToStore = value instanceof Function ? value(storedValue) : value;
setStoredValue(valueToStore);
if (typeof window !== 'undefined') {
window.localStorage.setItem(key, JSON.stringify(valueToStore));
}
} catch (error) {
console.error(error);
}
}, [key, storedValue]);
return [storedValue, setValue] as const;
};
使用自定义 Hook
typescript
const [notes, setNotes] = useLocalStorage<Note[]>('notes', []);
const addNote = (content: string) => {
const newNote = {
id: Date.now(),
content,
createdAt: new Date().toISOString()
};
setNotes(prev => [...prev, newNote]);
};
代码组织实践
文件夹结构
src/
├── app/ # Next.js 路由
│ ├── layout.tsx # 全局 Provider 注入点
│ └── page.tsx # 页面组件
├── components/ # 可复用 UI 组件
│ ├── button.tsx
│ └── todo-list.tsx
├── contexts/ # 全局状态
│ └── theme-context.tsx
├── hooks/ # 自定义 Hooks
│ └── use-local-storage.ts
└── types/ # TypeScript 类型定义
└── index.ts
重构原则
单一职责:一个文件只负责一种功能(UI、状态管理、副作用)提取逻辑:将重复逻辑提取到自定义 Hooks上下文分离:Context 定义单独放在contexts/文件夹,不在组件文件内定义类型安全:所有 Props、State、API 响应都必须有 TypeScript 类型定义
调试与 Vibe Coding 建议
避免 AI 编程陷阱
- 代码膨胀:定期重构,将混杂的代码按功能分到不同文件,避免 AI 在冗长文件中迷失上下文
- 命名冲突:确保 state、props、回调函数命名清晰,避免作用域混淆
- 渲染性能 :当出现卡顿或无限循环时,检查:
- 是否在渲染阶段直接调用
setState useEffect依赖数组是否正确- 是否使用了
useCallback/useMemo缓存频繁变化的函数/值
- 是否在渲染阶段直接调用
常见 Bug
| 现象 | 原因 | 解决 |
|---|---|---|
| 点击无反应,UI 不更新 | 直接修改变量而非使用 setState |
使用 useState 管理状态 |
| CPU 飙升,页面卡死 | 渲染阶段执行副作用导致无限循环 | 将副作用放入 useEffect |
| 列表状态错乱 | 未使用或错误使用 key |
使用数据唯一 ID 作为 key |
| 子组件过度渲染 | 每次渲染创建新函数引用 | 使用 useCallback 缓存回调 |
| 全局状态不生效 | Context Provider 包裹范围错误 | 在布局文件的根层级包裹 |
项目启动命令
bash
# 克隆仓库
git clone https://github.com/ShenSeanChen/yt-react-nextjs-tips.git
# 安装依赖
npm install
# 启动开发服务器
npm run dev
# 访问 http://localhost:3000
AI 编程-TypeScript 生产级代码
AI 时代,传统的软件工程 fundamentals 非但没有过时,反而是与 AI 协作的基石。 许多开发者误以为 AI 是全新的范式,但 Matt 发现,那些在人类协作中被证明有效的软件工程实践------保持任务小型化、持续反馈循环、垂直切片开发------同样适用于人机协作
一、警惕 LLM 的「智能区」与「愚蠢区」
1.1 二次方衰减现象
LLM 处理 token 时存在二次方衰减(quadratic scaling)问题。每个新增 token 都会与所有现有 token 建立注意力关系,这类似于足球联赛每增加一支球队,比赛场次会呈几何级增长。
text
Token 数量 → 注意力关系数量呈二次方增长
阈值 :无论你的上下文窗口是 200K 还是 1M,LLM 在约 100K tokens 时开始进入「愚蠢区」,表现为:
- 响应变得迟钝
- 开始做出错误决策
- 代码质量显著下降
1.2 应对:压缩与重置
| 策略 | 描述 | 偏好 |
|---|---|---|
| Compacting(压缩) | 将会话历史压缩成摘要,保留关键信息 | 讨厌这种方式 |
| 重置(Reset) | 清空上下文,从系统提示重新开始 | 像《记忆碎片》男主一样工作 |
理由:重置后状态永远是干净的初始状态,可预测、可控。如果能够为此优化工作流,你会处于最佳状态。
1.3 会话的生命周期
┌─────────────┐ ┌──────────────┐ ┌──────────────┐ ┌────────────┐
│ System │────▶│ Exploratory │────▶│ Implementation│────▶│ Testing │
│ Prompt │ │ Phase │ │ Phase │ │ Phase │
│ (保持最小) │ │ (AI 探索代码库) │ │ (编码实现) │ │ (反馈循环) │
└─────────────┘ └──────────────┘ └──────────────┘ └────────────┘
│ │
└──────────────── 重置 ◀───────────────────────┘
原则 :系统提示应尽可能小(曾见过有人塞入 250K tokens),否则直接进入「愚蠢区」。
二、用「拷问模式」达成设计共识
2.1 拒绝「规范即代码」
反对「Specs to Code」运动------即写一份详细规范文档,丢给 AI 转成代码,期待通过修改规范来间接改进代码。
text
规范 → AI 转代码 → 发现问题 → 修改规范 → 重复...
这种「vibe coding」的变体不工作,原因:
- 代码是你的主战场,你必须理解它
- 规范永远无法覆盖所有边界情况
- AI 会在代码中累积错误,而这些错误在规范中是看不到的
2.2 Grill Me 技能:强制对齐
markdown
# Grill Me Skill
Interview me relentlessly about every aspect of this plan until we reach a shared understanding.
Walk down each branch of the design tree.
Resolving dependencies one by one.
For each question, provide your recommended answer.
Ask the questions one at a time.
原理:
- 人类将需求/想法传给 AI,AI 使用这个技能
- AI 开始无情盘问需求的每个细节
- 每个问题附带 AI 的推荐答案
- 人类只需回应「
同意/不同意/补充」 - 循环继续,直到达成「
共享设计概念」
案例:
Q: 积分经济系统------哪些行为获得积分,各获得多少?
A: 建议保持简单,积分来源从少量开始
Q: 积分是否应该追溯(retroactive)?
现有 lesson progress records 该如何处理?
A: 建议非追溯
Q: 等级系统的成长曲线如何设计?
A: [AI 给出具体数值建议]
2.3 优势
| 传统做法 | 拷问模式 |
|---|---|
| 写完规范就开工 | 强制思考所有细节 |
| 遗漏边界情况 | 提前暴露数据回填等棘手问题 |
| 人类独自思考 | AI 作为追问者,强制你深入 |
| 规范可能过时 | 形成活的设计对话记录 |
关键 :目标不是一份漂亮的规范文档,而是与 AI 在同一波长 上------就是 Frederick P. Brooks 所说的「design concept」。
2.4 注意
- 拷问会话可以很长:记录过 40、80、甚至 100 个问题
- 适合团队使用:可以邀请领域专家,收集他们的意见后送入 AI 拷问
- 可以自定义:如果觉得 AI 问得太密集,可以调整技能让它收敛
三、拒绝水平分层,采用垂直切片
3.1 AI 的天然倾向:水平编码
观察到一个关键现象:AI 喜欢按水平顺序编码。
Phase 1: 全部数据库/Schema 修改
Phase 2: 全部 API 层修改
Phase 3: 全部前端修改
...
这种方式的问题:直到 Phase 3,你才能获得集成反馈 。AI 在盲目编码中累积错误,完全看不到系统是否真正 work。
3.2 垂直切片:「示踪弹」原则
来自《The Pragmatic Programmer》的 Tracer Bullets 概念:
想象你是防空炮手,夜间向飞机开火。普通子弹你看不到落点。示踪弹在尾部涂有荧光物质,能让你看到弹道轨迹,从而实时调整瞄准。
垂直切片 (Vertical Slices)就是跨越所有层的薄功能切片
┌────────────────────────────────────────────┐
│ 垂直切片 1: 课程完成 → 积分增加 → Dashboard显示 │
│ (Database + API + Frontend 全部包含) │
└────────────────────────────────────────────┘
优势:
- 每个阶段结束都能获得可运行的
集成反馈 - AI 不再是盲目编码,而是在「打示踪弹」
- 人类可以早期干预,
防止错误累积
3.3 PRD to Issues 技能
markdown
Break a PRD into independently grabable issues using vertical slices (tracer bullets).
- First locate the PRD
- Explore the code base (if fresh session)
- Draft vertical slices
- Quiz the user for confirmation
- Create issue files
关键:
- 每个 issue 应该是**跨越所有层**的功能薄片
- 好的第一个切片示例:「课程完成时奖励积分并在 Dashboard 显示」
- 不好的第一个切片:「只创建 gamification service」(这是水平的)
3.4 Kanban Board 的并行化
拆分后的 issues 形成带有阻塞关系的看板:
┌─────────────────────────────────────────────────────┐
│ Issue #1: Schema + Gamification Service [无阻塞] │
│ │ │
│ ├──▶ Issue #2: Points/Streaks tracking [阻塞#1] │
│ ├──▶ Issue #3: Wire into lessons [阻塞#1] │
│ │ │
│ └──▶ Issue #4: Retroactive backfill [阻塞#1] │
│ │
│ └──▶ Issue #5: Dashboard integration [阻塞#2,3,4] │
└─────────────────────────────────────────────────────┘
价值 :可以实现真正并行化 。多个 agent 可以同时在不同 phase 工作:
Phase 1: #1 (单 agent)
Phase 2: #2, #3 (并行 agents)
Phase 3: #4, #5 (并行 agents)
对比顺序多阶段计划只能被单一 agent 使用,Kanban 方案效率高得多。
四、TDD 是 AI 代理不可或缺的导航仪
4.1 没有反馈循环的 AI 只能盲目编码
核心:
反馈循环的质量直接决定了 AI 输出的上限。糟糕的代码库往往源于糟糕的自动化测试。
4.2 红-绿-重构(Red-Green-Refactor)
text
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ RED │───▶│ GREEN │───▶│ REFACTOR │
│ 写失败测试 │ │ 让测试通过 │ │ 改进代码 │
└─────────────┘ └─────────────┘ └─────────────┘
│ │
└──────────── 循环往复 ◀────────────────┘
问题 :AI 倾向于在实现后再写测试,这会导致:
- AI 在测试中「作弊」------因为已经知道实现细节
- 测试质量低,难以捕获真实 bug
- 反馈循环延迟
TDD 的优势:
- 先写测试 = 在编写实现前先「规范」期望行为
- AI 必须思考「这个模块应该做什么」才能写测试
- 大幅
减少作弊可能性
4.3 实践
AI 正在开发 gamification service:
1. 写一个失败的测试 (RED)
"AwardPointsForLessonCompletion should add 100 points
when a lesson is completed"
2. 运行测试 → 失败(因为模块尚不存在)
3. 实现功能 (GREEN)
创建 gamification service,添加逻辑
4. 运行测试 → 通过
5. REFACTOR(可选)
改进实现,保持测试通过
4.4 人类 QA:不可替代
虽然 AI 可以自动化测试,但 QA 阶段必须有人类参与:
- AI 生成的前端往往缺乏「品味」
- 自动化测试无法检测「感觉不对」
- 人类的审美和判断是防止「Slop」的关键
text
QA = 人类将主观偏好「按回」代码库的唯一机会
五 AI 友好型代码
5.1 浅层模块 vs 深层模块
John Ousterhout 在《A Philosophy of Software Design》中定义了两种模块类型:
| 类型 | 特征 | AI 友好度 |
|---|---|---|
| 浅层模块 | 大量小文件,复杂依赖图,每个文件导出很少的东西 | 差------AI 难以追踪依赖关系 |
| 深层模块 | 接口简单(少量公共 API),内部逻辑丰富 | 好------AI 只需关注接口契约 |
5.2 为什么深层模块更适合 AI
浅层模块 (Bad):
┌───┐ ┌───┐ ┌───┐
│ A │──▶│ B │──▶│ C │
└───┘ └───┘ └───┘
│ │ │
▼ ▼ ▼
┌───┐ ┌───┐ ┌───┐
│ D │ │ E │ │ F │
└───┘ └───┘ └───┘
问题:AI 必须理解 A→B→D 的完整调用链,
每个模块的测试边界在哪里?
深层模块 (Good):
┌──────────────────┐
│ Gamification │
│ Service │
│ ┌──────────────┐ │
│ │ Interface │ │ ← 简单接口
│ │ AwardPoints │ │
│ │ GetLevel │ │
│ └──────────────┘ │
│ ┌──────────────┐ │
│ │ Internals │ │ ← 丰富逻辑
│ │ (委托给 AI) │ │
│ └──────────────┘ │
└──────────────────┘
优势:AI 可以只关注接口行为,
内部实现安全委托给 AI 代理
5.3 人类的认知平衡
观察:使用 AI 后,开发者普遍感觉:
- 工作更努力了(速度提升带来的压力)
- 对代码库的了解反而变差了
解决方案:设计深层模块,让接口成为你的「认知边界」:
人类职责:设计接口,理解「模块做什么」
AI 职责:实现接口内部的丰富逻辑
这样你既保持了对整体架构的掌控,又不用深入每个实现细节。
5.4 Improve Codebase Architecture 技能
Matt 提供了一个分析代码库、识别可深化模块的技能:
bash
# 在 Claude Code 中调用
/improve-codebase-architecture
它会扫描代码库,识别:
- 相互耦合但可作为整体测试的模块簇
- 当前缺少测试的深层模块候选
- 局部可替代性(Local Substitute)机会
真实案例:Matt 的视频编辑器模块原本分散在前后端,使用这个技能后,他找到了一种用 discriminated union 包裹前后端类型的方式,创建了一个可以端到端测试的深层模块。AI 在这个模块中工作的能力产生了「night and day」级别的提升。
六、白班规划、夜班执行的并行工作流
6.1 工作流总览
┌──────────────────────────────────────────────────────────────────┐
│ HUMAN DAY SHIFT │
│ ┌─────────┐ ┌──────────┐ ┌───────────┐ ┌─────────────────┐ │
│ │ 想法/ │──▶│ Grill Me │──▶│ Write PRD │──▶│ PRD → Kanban │ │
│ │ 需求 │ │ (拷问) │ │ (目的地) │ │ (可并行 issues) │ │
│ └─────────┘ └──────────┘ └───────────┘ └─────────────────┘ │
│ │ │
│ │ 人类审查、确认、打磨设计概念 │
└────────┼────────────────────────────────────────────────────────┘
│ 交接
▼
┌──────────────────────────────────────────────────────────────────┐
│ AI NIGHT SHIFT (AFK) │
│ ┌────────────────────────────────────────────────────────────┐ │
│ │ Ralph Loop / Sandcastle │ │
│ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ │
│ │ │ Planner │───▶│Implement │───▶│ Reviewer │──┐ │ │
│ │ │ (规划) │ │ (实现) │ │ (审查) │ │ │ │
│ │ └──────────┘ └──────────┘ └──────────┘ │ │ │
│ │ │ │ │
│ │ ┌──────────┐ │ │ │
│ │ │ Merger │◀────────────────┘ │ │
│ │ │ (合并) │ │ │
│ │ └──────────┘ │ │
│ └────────────────────────────────────────────────────────────┘ │
│ │
│ Docker Sandbox 隔离执行环境 │
│ 多 agent 并行处理不同 phase │
└──────────────────────────────────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────────────────────────┐
│ HUMAN QA LOOP │
│ 手动检查 → 反馈到 Kanban → 新 issues 生成 │
│ 迭代直到满意 │
└──────────────────────────────────────────────────────────────────┘
6.2 两种任务类型
| 类型 | 特点 | 适合谁来执行 |
|---|---|---|
| Human-in-the-Loop | 需要人类判断、对齐、审查 | 人类(规划、设计、QA) |
| AFK (Away From Keyboard) | 可以委托执行,不需要实时监控 | AI agent |
关键洞察:
- 规划和对齐阶段必须是 Human-in-the-Loop
- 纯实现阶段可以变成 AFK 任务
- 人类的价值在于:构思、对齐、质量保证
6.3 Sandcastle:并行 agent 执行框架
Matt 开发了一个 TypeScript 库 Sandcastle,用于管理 AFK agent 工作流:
typescript
// 主流程
const workTree = await run(workDir); // 创建工作树
const sandbox = await workTree.createSandbox(); // Docker 隔离
const result = await sandbox.run(prompt); // 执行 prompt
await workTree.commit(result); // 提交结果
优势:
- 每个 agent 在 Docker 容器中隔离运行
- 工作树本质上是 git branch,可以后续 merge
- 支持多 agent 并行处理不同 issues
6.4 Pull vs Push:编码标准的传递
| 方式 | 机制 | 适用场景 |
|---|---|---|
| Push | 将指令推送给 agent(通过 CLAUDE.MD 等) | 自动化的 reviewer(需要知道自己必须检查什么) |
| Pull | agent 主动拉取(skills 文件) | 实现者(遇到问题时查阅规范) |
Matt 的策略:
- 实现者:允许 Pull(遇到问题可以查阅 skills)
- 审查者:Push 编码标准(强制检查是否遵循)
七、实用工具与技能速查
7.1 Matt 推荐的核心技能
| 技能名称 | 用途 |
|---|---|
grill-me |
强制对齐,生成设计概念 |
write-prd |
创建产品需求文档 |
prd-to-issues |
将 PRD 拆解为可并行的 Kanban issues |
improve-codebase-architecture |
分析代码库,识别可深化的模块 |
7.2 Ralph Prompt 核心指令
markdown
# 实现者指令
Local issue files from `issues/` are provided at the start of context.
Work on the AFK issues only.
If all AFK tasks are complete, output "No more tasks."
Pick the next task prioritizing: Critical Bug Fixes >
Development Infrastructure > Tracer Bullets > Polish >
Quick Wins > Refactors.
Explore the repo. Use TDD. Complete the task.
7.3 监控指标
始终关注 token 使用量------这是你与「愚蠢区」距离的唯一指标:
┌─────────────────────────────────────┐
│ Tokens: 25,847 / 200,000 [12.9%] │
└─────────────────────────────────────┘
八、Matt 的终极建议
8.1 关于 PRD 的处理
不要阅读 PRD 文档。
理由:你已经在 Grill Me 中与 AI 达成对齐,阅读 PRD 只在测试 AI 的「摘要能力」。你真正应该做的是信任对齐过程,把时间花在 QA 上。
8.2 关于文档管理
不要保留旧 PRD。这会引发「文档腐烂」(Doc Rot)------过时的文档会继续影响 AI 的行为,但代码早已面目全非。
建议:关闭 GitHub issues,但保留关闭状态作为历史记录。
8.3 关于并行化
Kanban 看板 > 顺序多阶段计划。前者允许真正的并行化,后者只能被单一 agent 使用。
8.4 关于代码审查
AI 会让你做更多的代码审查。Matt 坦言这是现实:当你把编码委托给 agent,你需要花更多时间在审查上。这不是坏事,只是需要调整预期。
九、延伸阅读
Matt 推荐的传统软件工程经典著作:
- 《The Pragmatic Programmer》--- Tracer Bullets、垂直切片概念
- 《Refactoring》--- Martin Fowler 的任务分解原则
- 《A Philosophy of Software Design》--- John Ousterhout 的深层模块理论
- 《The Design of Design》--- Frederick P. Brooks 的设计概念论述
核心结论:在 AI 时代,return to basics。20 年前的软件工程最佳实践在今天非但没有过时,反而是挖掘 AI 潜力的最佳工具。