AI时代下的TypeScript

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. 总结

  1. 类型注解优先 :始终为函数参数和返回值添加类型,这是TS提供80%价值的地方
  2. const优先 :默认使用const,必要时使用let,永远不用var
  3. 严格相等 :始终使用===!==
  4. 早期返回:用早期返回替代深层嵌套的if/else
  5. 单一职责:函数和模块都应专注于单一任务
  6. 避免默认导出:使用命名导出提高代码可维护性
  7. 利用类型推断:简单变量无需冗余注解,让TS自动推断

后续:掌握基础后,应学习接口(Interfaces)、泛型(Generics)、类(Classes)和高级类型操作(Mapped Types、Conditional Types)构建企业级应用

React/TypeScript

掌握 React/TypeScript 开发中的实际使用场景:

  • 状态管理useStateuseEffectuseCallbackuseMemo
  • 组件架构: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

重构原则

  1. 单一职责:一个文件只负责一种功能(UI、状态管理、副作用)
  2. 提取逻辑:将重复逻辑提取到自定义 Hooks
  3. 上下文分离 :Context 定义单独放在 contexts/ 文件夹,不在组件文件内定义
  4. 类型安全:所有 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」的变体不工作,原因:

  1. 代码是你的主战场,你必须理解它
  2. 规范永远无法覆盖所有边界情况
  3. 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.

原理

  1. 人类将需求/想法传给 AI,AI 使用这个技能
  2. AI 开始无情盘问需求的每个细节
  3. 每个问题附带 AI 的推荐答案
  4. 人类只需回应「同意/不同意/补充
  5. 循环继续,直到达成「共享设计概念

案例

复制代码
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 潜力的最佳工具。

相关推荐
sulikey1 小时前
大模型是如何工作的
人工智能
久违 °8 小时前
【AI-Agent】TagMatrix 数据标注工具开发
人工智能·数据分析·go·agent·数据隐私
AI360labs_atyun9 小时前
腾讯推出电子牛马Marvis,好用吗?
人工智能·科技·ai
Dfreedom.9 小时前
Windows、虚拟机、开发板组网通信原理及调试通联步骤
人工智能·windows·部署·边缘计算·开发板·模型加速
3DVisionary9 小时前
蓝光三维扫描:医疗制造的精度焦虑怎么解
人工智能·算法·制造·蓝光三维扫描·医疗制造·三维检测·义齿检测
Are_You_Okkk_9 小时前
基于MonkeyCode解析AI研发新模式,根治开发低效痛点
大数据·人工智能·开源·ai编程
好评笔记9 小时前
机器学习面试八股——常用损失函数
人工智能·深度学习·算法·机器学习·校招
weixin_468466859 小时前
全局与局部注意力机制新手实战指南
人工智能·python·深度学习·算法·自然语言处理·transformer·注意力机制
weixin_468466859 小时前
工业相机成像原理新手入门指南
人工智能·自动化·机器视觉·工业相机·光学·光学系统·成像原理