RenOS
学习笔记 ·

从vibe coding产品反向学习 TypeScript:Agent 方向代码阅读路线

本文目录 24 节
  1. 先看结论
  2. 推荐阅读路径
  3. 一眼判断某段代码该重点看什么
  4. Agent 数据流
  5. TypeScript 类型系统
    1. type、const、let 的区别
  6. JavaScript 对象语法
  7. 函数语法
    1. 函数签名怎么读
  8. 异步编程
  9. React 和 Hooks
  10. Expo 和 React Native 够用知识
  11. Agent 业务概念
  12. Supabase 和 Edge Function
  13. 错误处理和运行时校验
  14. 从测试反向学习
  15. 读代码时的检查问题
  16. 学习优先级
    1. P0:必须先懂
    2. P1:读 Agent 时很快会用到
    3. P2:暂时不用急
  17. 后续扩写计划
  18. 术语小抄
  19. 最短总结

这篇文章不是一份完整的 TypeScript 语法手册,而是一份“反向读项目”的学习路线。

我现在更关心的问题不是“TypeScript 有多少语法”,而是:当我打开一个真实项目时,能不能看懂这些代码为什么这么写、数据怎么流动、哪里是页面、哪里是业务逻辑、哪里是真正写入数据的地方。

WayLog 这个项目很适合做这件事。它是一个 React Native + Expo + TypeScript 项目,里面有旅行行程、Agent 对话、Supabase、AsyncStorage、本地优先同步等真实业务代码。尤其是 Agent 这一块,刚好能把 TypeScript、异步编程、React Hooks 和安全写入协议串起来。

先看结论

WayLog 当前的代码风格很典型:

观察点项目里的情况学习时该怎么理解
类型声明大量使用 type主要用来描述数据形状,编译后消失
运行时代码大量使用 const,少量 let真正运行时存在的变量、函数、对象
很少用 class主要用于自定义错误类,不是 OOP 风格项目
业务组织方式函数 + 普通对象 + React Hooks现代 React / React Native 常见写法
异步编程非常多Agent、存储、同步、Supabase、外部 API 都靠异步
Agent 写入先生成提案,再确认写入LLM 不能直接改 Trip,这是核心安全边界

最关键的一条学习链路是:

TypeScript 类型
-> JavaScript 对象和函数
-> async/await
-> React Hooks 状态流
-> AgentProposal 安全协议
-> Trip 本地优先写入链路

如果目标是读懂 WayLog 的 Agent,不需要一开始就把 Expo、React Native 原生模块、构建发布、地图 SDK 都学完。应该先把这条链路打通。

推荐阅读路径

我会优先按真实调用链读,而不是从最大的页面文件硬啃。

app/agent.tsx
-> domains/agent/screens/conversation/agent-conversation-screen.tsx
-> features/agent/*
-> supabase/functions/agent-turn/*
-> features/trips/commands.ts
-> features/trips/storage.ts

这条路径对应的是:

  1. app/agent.tsx:Agent 页面入口。
  2. domains/agent/screens/conversation/agent-conversation-screen.tsx:页面状态和用户交互。
  3. features/agent/agent-turn-client.ts:客户端怎么请求 Agent。
  4. supabase/functions/agent-turn/index.ts:服务端怎么生成结构化结果。
  5. features/agent/types.ts:Agent 提案的数据结构。
  6. features/trips/commands.ts:提案如何变成 Trip 修改。
  7. features/trips/storage.ts:最终如何写入本地存储并触发同步。

一眼判断某段代码该重点看什么

读 TypeScript 项目时,不要看到所有语法都平均用力。可以先建立一张“识别表”:

你看到的语法说明阅读重点
type Xxx = { ... }类型别名它描述什么数据形状
const xxx = ...运行时常量它保存什么值或函数
async function异步函数它等待什么 IO,错误怎么处理
await xxx()等 Promise 完成后续代码依赖这个结果
useState(...)React 页面状态哪些用户操作会改变它
useEffect(...)副作用加载、监听、清理逻辑在哪里
{ ...trip }对象复制是否避免原地修改
value is Xxx类型守卫是否在运行时校验未知数据
Promise.all(...)并发任务任意失败是否会让整体失败
try/catch/finally错误和清理失败时页面或数据是否回滚

Agent 数据流

WayLog 的 Agent 不是“模型说什么就做什么”。它更像是一条受控流水线:

用户输入消息
-> AgentConversationScreen 处理页面状态和事件
-> features/agent/agent-turn-client.ts 请求 Agent Turn
-> Supabase Edge Function: agent-turn
-> LLM / Skill 生成结构化结果
-> AgentProposal,只是一份提案
-> 客户端运行时校验 proposalId / operationId / expectedUpdatedAt
-> UI 预览影响范围
-> 用户确认
-> features/trips/commands.ts 应用领域命令
-> features/trips/storage.ts 写入 AsyncStorage
-> dirty 标记
-> 后台同步 Supabase

核心记忆点:

LLM 只能生成 AgentProposal,不能直接修改 Trip。真正写入发生在用户确认之后。

TypeScript 类型系统

WayLog 大量使用 type 来描述数据形状,很少用传统 OOP 的 class。这很符合 React Native + TypeScript 的常见写法。

优先需要理解这些语法:

  • type Xxx = { ... }:声明对象的形状。
  • 联合类型:"success" | "error" | "loading"
  • 可选属性:title?: string
  • 数组类型:Trip[]
  • 泛型:Promise<T>requestSupabase<T>()
  • Record<string, unknown>:任意 key 的对象。
  • Partial<T>:某个类型的部分字段。
  • ReturnType<typeof fn>:从函数推导返回类型。
  • import type { Xxx }:只导入类型,运行时不存在。

比如 Agent 操作类型可能长这样:

export type AgentTurnProposalKind =
| "add_place_to_day"
| "move_day_item"
| "remove_day_item"
| "update_day_item";

这表示 AgentTurnProposalKind 只能是这 4 个字符串之一。好处是:

  • 写错字符串时 TypeScript 会报错。
  • 代码阅读者马上知道 Agent 当前支持哪些操作。
  • 后续 if / switch 可以按这几个分支处理。

再看一个输入类型:

export type RequestAgentTurnInput = {
accessToken?: string;
dayId?: string;
itemId?: string;
kind: AgentTurnProposalKind;
trip: Trip;
userMessage?: string;
};

读这个类型时,不要只翻译字段名,而要问:

  • 哪些字段必填?kindtrip 必填。
  • 哪些字段可选?带 ? 的字段可选。
  • 哪个字段决定行为?kind
  • 哪个字段是业务主体?trip

typeconstlet 的区别

我之前容易把“声明”这个词混在一起看。其实 typeconstlet 根本不在同一层。

语法属于哪一层编译后还在吗用途
typeTypeScript 类型层不在描述数据形状
interfaceTypeScript 类型层不在描述对象或类契约
constJavaScript 运行时声明不可重新赋值的变量
letJavaScript 运行时声明可重新赋值的变量
classJavaScript 运行时创建类、继承、实例

例子:

type TripSummary = {
id: string;
title: string;
};
const trip: TripSummary = {
id: "trip-1",
title: "东京五日游",
};

这里:

  • TripSummary 是类型,运行时不存在。
  • trip 是真实对象,运行时存在。
  • : TripSummary 是给 trip 加类型约束。

如果编译成 JavaScript,大致只剩:

const trip = {
id: "trip-1",
title: "东京五日游",
};

所以不能说“项目里都是 type,没有 const”。更准确的说法是:项目在类型层大量用 type,在运行时大量用 const

JavaScript 对象语法

WayLog 的核心业务数据几乎都是普通对象:

  • Trip
  • TripDay
  • TripDayItem
  • AgentProposal
  • PendingAgentProposal
  • AgentConversationMessage

这说明项目不是“class 实例满天飞”的写法,而是“数据对象 + 函数处理”的写法。

需要掌握的对象语法:

语法示例含义
对象字面量{ id, title }创建对象
属性访问trip.id读取字段
解构const { id } = trip从对象取字段
对象展开{ ...trip, title: next }复制并覆盖字段
数组展开[...items, nextItem]创建新数组
可选链trip.owner?.name中间为空时不报错
空值合并title ?? "未命名"只在 null / undefined 时兜底
模板字符串`trip-${id}`拼接字符串

项目中常见的更新模式是:

const updatedTrip = {
...trip,
updatedAt: new Date().toISOString(),
};

这不是修改原对象,而是基于旧对象创建一个新对象。

这在 React 里很重要,因为 React 通常靠“引用变化”判断状态是否更新。对 WayLog 来说也很重要,因为 Trip 写入链路要尽量避免函数偷偷原地修改传入对象。

常见易错点:

易错写法问题
trip.title = "新标题"可能原地修改,影响不可预测
items.push(nextItem)原地改数组,React 状态里容易出问题
{ trip, title: next }这不是展开,会生成 { trip: ..., title: ... }
`value

更推荐:

const nextTrip = {
...trip,
title: "新标题",
};
const nextItems = [...items, nextItem];

函数语法

WayLog 的业务逻辑主要靠函数组织,而不是靠类。你会看到:

  • 纯函数:输入数据,返回新数据。
  • 异步函数:请求网络、读写存储。
  • React 事件函数:响应用户点击。
  • 回调函数:传给 mapfilteruseEffectsetState
  • 高阶函数:接收另一个函数作为参数。

普通函数:

function normalizeTrip(trip: Trip): Trip {
return trip;
}

箭头函数 + 异步:

const submitMessage = async () => {
const result = await requestAgentTurn(input);
return result;
};

回调函数:

const titles = trip.days.map((day) => day.title);

高阶函数:

async function withAgentConversationMutationQueue<T>(
conversationId: string,
task: () => Promise<T>,
): Promise<T> {
await previousTail.catch(() => undefined);
return await task();
}

这里最关键的是 task: () => Promise<T>。它的意思是:参数 task 本身是一个函数,调用后返回 Promise<T>

函数签名怎么读

看到这个类型:

type PersistTripUpdate = (
nextTrip: Trip,
fallbackTrip: Trip,
errorMessage?: string,
) => Promise<boolean>;

可以逐段读:

  • PersistTripUpdate 是一个函数类型。
  • 它接收 3 个参数。
  • nextTripfallbackTrip 必填。
  • errorMessage 可选。
  • 返回 Promise<boolean>,说明这是异步函数,最终结果是布尔值。

异步编程

如果要搞 Agent,异步编程是核心能力。Agent 涉及:

  • 模型请求
  • Supabase Edge Function
  • AsyncStorage 本地存储
  • 云同步
  • POI 搜索
  • 天气请求
  • 超时取消
  • 错误处理

这些都不是立即返回结果的计算,而是 IO。IO 在 TypeScript / JavaScript 里通常用 Promise 表达。

必须掌握的异步语法:

语法含义
async function声明异步函数
await promise等 Promise 完成
Promise<T>未来会得到一个 T
try/catch捕获失败
finally无论成功失败都执行清理
Promise.all()并发执行,任意失败则整体失败
Promise.allSettled()并发执行,保留每个任务成功/失败
AbortController取消请求
setTimeout()延迟或超时
fetch()发 HTTP 请求

典型请求流程:

export async function requestSupabase<T>(
options: SupabaseRequestOptions,
): Promise<T> {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 30000);
try {
const response = await fetch(url, {
method: "POST",
signal: controller.signal,
});
return (await response.json()) as T;
} finally {
clearTimeout(timeoutId);
}
}

重点:

  • requestSupabase<T> 是泛型异步函数。
  • fetch 返回 Promise<Response>
  • await response.json() 也返回 Promise。
  • finally 里清理 timeout,避免残留。
  • as T 是类型断言,不等于运行时校验。

异步错误处理也很重要:

try {
const result = await requestAgentTurn(input);
setMessages((current) => [...current, result.message]);
} catch (error) {
const message =
error instanceof Error ? error.message : "Agent 请求失败";
setActionError(message);
}

读这种代码时问 4 件事:

  1. 哪一步可能失败?
  2. 失败时 UI 怎么显示?
  3. 是否会留下半个状态?
  4. 是否需要回滚或重试?

常见易错点:

易错点正确理解
忘记 await得到的是 Promise,不是真实结果
map 里写 async 但不 await Promise.all任务可能没等完
以为 try/catch 能捕获没 await 的 Promise不能稳定捕获
Promise.all 用在可容忍失败的场景一个失败会导致整体失败
超时后不清理 timer会造成多余回调

React 和 Hooks

只想搞 Agent,也需要懂 React 的页面状态流。否则会看不懂:

  • 用户点发送后,消息怎么进入页面。
  • 请求中状态怎么显示。
  • Agent 提案为什么会先进入“待确认”状态。
  • 用户确认后为什么要更新 Trip。
  • 错误为什么会显示在弹窗或提示区。

必须先懂这些 Hooks:

Hook用途在 Agent 页面里的意义
useState页面状态消息、错误、待确认提案、加载状态
useEffect副作用加载会话、监听数据变化、清理状态
useMemo缓存计算避免每次渲染都重复算派生数据
useCallback缓存函数事件函数稳定传给子组件
useRef保存可变值防重复提交、记录正在执行的状态

典型代码:

const [messages, setMessages] = useState<AgentConversationMessage[]>([]);
useEffect(() => {
void loadConversation();
}, []);
const submitMessage = async () => {
const result = await requestAgentTurn(input);
setMessages((current) => [...current, result.message]);
};

要点:

  • messages 是当前状态。
  • setMessages 用来更新状态。
  • useEffect(..., []) 通常表示组件挂载时执行一次。
  • void loadConversation() 表示故意不等待这个 Promise,但要明确知道错误在哪里处理。
  • setMessages((current) => ...) 用当前最新状态计算下一状态。

React 页面不是普通脚本。普通脚本像这样:

第一行 -> 第二行 -> 第三行 -> 结束

React 页面更像这样:

初次渲染
-> 用户点击
-> 事件函数执行
-> setState
-> 重新渲染
-> useEffect 可能执行
-> 再等待下一次用户事件

所以读 React 文件时,不要只从上到下一行行读。要找:

  • 状态在哪里声明?
  • 状态在哪里被修改?
  • 用户事件在哪里触发?
  • 异步结果回来后改了什么状态?
  • 子组件接收了哪些 props?

Expo 和 React Native 够用知识

如果重点是 Agent,不需要一开始深学 Expo。先知道这些就够了:

知识点够用理解
app/ 文件路由文件路径决定页面路由
app/agent.tsxAgent 页面入口
useLocalSearchParams读取路由参数
View容器
Text文本
Pressable可点击区域
ScrollView滚动区域
AsyncStorage手机本地 key-value 存储
EXPO_PUBLIC_*会暴露给客户端的环境变量

暂时不用优先深学:

  • Android / iOS 原生模块。
  • Expo 构建发布。
  • 推送通知。
  • 动画系统。
  • 地图 SDK 深入。
  • 应用签名和商店发布流程。

Agent 业务概念

这是我最终要重点掌握的部分。

WayLog 的 Agent 不是“模型说什么就做什么”。它有一个安全协议:

模型输出
-> AgentProposal
-> 客户端校验
-> 用户预览
-> 用户确认
-> Trip 命令
-> 本地写入
-> 云同步

核心概念:

概念作用
AgentProposalAgent 生成的提案
proposalId提案唯一 ID
operationId单个操作唯一 ID
expectedUpdatedAt防止基于旧 Trip 修改
runtime validation运行时校验模型返回
idempotency防止重复应用同一操作
confirm before write默认用户确认后才写入
local-first先写本地,再同步云端

为什么不能让 LLM 直接写 Trip?

因为模型输出是不可信的。它可能:

  • 指向错误的 tripId
  • 基于旧版本 Trip 生成修改。
  • 删除用户不想删的内容。
  • 生成重复操作。
  • 漏掉必填字段。
  • 把外部资料里的提示词当成系统指令。

所以 WayLog 的策略是:

  1. LLM 只生成结构化提案。
  2. 客户端校验提案。
  3. UI 展示影响范围。
  4. 用户确认。
  5. 客户端用受控命令修改 Trip。

重点文件索引:

文件阅读重点
features/agent/types.tsAgentProposal 数据结构
features/agent/agent-turn-client.ts客户端请求 Agent Turn
features/agent/applied-proposals.ts已应用操作记录,防重复
features/agent/runtime-tool-router.ts工具路由和运行时边界
domains/agent/screens/conversation/agent-conversation-screen.tsx页面状态和交互
supabase/functions/agent-turn/index.tsEdge Function 入口
supabase/functions/agent-turn/llm-client.tsLLM 请求
supabase/functions/agent-turn/skills/add-place.ts添加地点 skill
features/trips/commands.tsTrip 领域命令
features/trips/storage.ts本地优先写入

Supabase 和 Edge Function

Agent 服务端运行在 Supabase Edge Function 中。不需要一开始成为 Supabase 专家,但需要懂 HTTP 和服务端函数基本结构。

需要掌握:

  • Deno.serve(...)
  • Request / Response
  • await request.json()
  • HTTP status:200400401405500
  • Authorization header
  • JWT 用户身份
  • Edge Function 调 LLM
  • JSON 响应结构

典型结构:

Deno.serve(async (request) => {
if (request.method !== "POST") {
return new Response("Method not allowed", { status: 405 });
}
const body = await request.json();
const result = await requestAgentLLMJson(body);
return new Response(JSON.stringify(result), {
headers: { "Content-Type": "application/json" },
});
});

读 Edge Function 时重点问:

  • 它允许哪些 HTTP 方法?
  • 它从哪里拿用户身份?
  • 它有没有信任客户端传来的 userId
  • 它有没有直接写用户 Trip?
  • 它返回的是自由文本还是结构化 JSON?

错误处理和运行时校验

Agent 场景里,错误处理不是附属内容,而是安全边界的一部分。

TypeScript 类型只能约束自己写的代码,不能天然保证:

  • 网络响应一定符合类型。
  • LLM 返回一定符合类型。
  • AsyncStorage 里的历史数据一定没坏。
  • 外部 API 一定正常。

所以项目需要运行时校验。

项目里的 TypeScript class 很少,典型用途是自定义错误:

export class AgentTurnClientError extends Error {
code: AgentTurnClientErrorCode;
}

这类 class 的作用不是组织业务对象,而是让错误更有结构:

  • error.message 给人看。
  • error.code 给程序判断。
  • error instanceof AgentTurnClientError 可以识别错误类型。

另一个重要语法是类型守卫:

function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === "object" && value !== null && !Array.isArray(value);
}

这类函数的价值是:

  1. 运行时真的检查了一次。
  2. TypeScript 在 if (isRecord(value)) 之后会把 value 缩窄成对象类型。

这比直接写 value as Record<string, unknown> 更安全。

常见风险:

写法风险
const data = await response.json() as AgentProposal只是告诉 TypeScript 相信你,没有真正校验
不检查 response.ok失败响应也可能被当成功数据处理
catch 后只 console.logUI 和调用方可能不知道失败了
忽略 finally加载状态、锁、timer 可能不清理

从测试反向学习

测试能告诉我一个函数“应该怎么用”。当我读不懂某个 Agent 函数时,去看测试通常比硬读实现更快。

重点目录:

  • tests/features/agent/
  • tests/features/trips/
  • tests/hooks/

常用命令:

Terminal window
pnpm test
pnpm typecheck
pnpm lint

Agent 操作测试应该覆盖:

场景为什么要测
正常应用确认 happy path
无效参数防止模型返回坏数据
目标不存在Trip 已变化时不误写
重复确认防止重复执行
版本冲突防止基于旧数据修改
中途失败防止留下半个 Trip

读代码时的检查问题

每读一个函数,都可以问自己:

  • 这个函数是同步还是异步?
  • 它接收什么类型的输入?
  • 它返回什么类型的结果?
  • 它有没有修改外部状态?
  • 它有没有写 AsyncStorage?
  • 它有没有调用 Supabase?
  • 它处理错误了吗?
  • 它是不是只生成提案,还是会真正写入 Trip?
  • 它依赖用户确认吗?
  • 它有没有防止重复执行?
  • 它有没有校验外部输入?
  • 它有没有可能基于旧 Trip 修改?

每读一个 React 页面,问:

  • 这个页面有哪些 useState
  • 哪些状态是加载态?
  • 哪些状态是错误态?
  • 哪些状态代表“待确认”?
  • 哪些函数由按钮触发?
  • 哪些函数会发请求?
  • 哪些函数会写入本地数据?
  • 哪些 props 传给了子组件?
  • 哪些逻辑应该抽到 features/

每读一个 Agent 文件,问:

  • 这里是在生成提案,还是在应用提案?
  • 这里有没有校验 proposalId
  • 这里有没有校验 operationId
  • 这里有没有校验 expectedUpdatedAt
  • 这里有没有处理重复操作?
  • 这里有没有绕过用户确认?
  • 这里有没有直接写云端 Trip?
  • 这里有没有信任外部内容?

学习优先级

P0:必须先懂

  • type 和对象类型。
  • const / let 和运行时代码。
  • 函数参数和返回值类型。
  • async / await / Promise
  • try/catch/finally
  • 对象展开和数组展开。
  • React useState / useEffect
  • AgentProposal 的安全边界。

P1:读 Agent 时很快会用到

  • 泛型:<T>
  • 类型守卫:value is Xxx
  • unknown 和运行时校验。
  • useCallback / useMemo / useRef
  • HTTP 请求和响应。
  • Supabase Edge Function。
  • AsyncStorage。
  • 测试中的 mock。

P2:暂时不用急

  • class/OOP 深度设计。
  • interface vs type 的所有细节争论。
  • 高级 TypeScript 类型体操。
  • React Native 原生模块。
  • Expo 发布和签名。
  • 动画系统。
  • 地图 SDK 深入。
  • iOS / Android 原生开发。

后续扩写计划

这篇文章只是总览。后续可以继续按章节展开:

顺序章节目标
1TypeScript 类型系统详解看懂 features/agent/types.ts
2函数语法详解看懂业务函数和 React 事件函数
3对象与不可变更新看懂 Trip 如何被复制和修改
4异步编程详解看懂请求、存储、同步
5React Hooks 详解看懂 Agent 页面状态流
6AgentProposal 协议详解看懂模型输出和安全校验
7Trip 写入链路详解看懂本地优先和云同步
8测试反向阅读从测试倒推业务规则

每章可以固定成这个结构:

概念解释
-> 项目中的真实例子
-> 易错点
-> 自测题
-> 建议阅读文件

术语小抄

术语简明解释
类型层TypeScript 编译期存在的东西,比如 type
运行时App 真正执行时存在的东西,比如 const 对象
Promise未来才会完成的异步结果
HookReact 函数组件里管理状态和副作用的函数
ProposalAgent 给出的修改提案
OperationProposal 里的一个具体操作
Runtime validation运行时校验外部数据
Idempotency同一个操作重复执行也不会造成重复写入
Local-first先写本地,再同步云端
Edge FunctionSupabase 上运行的服务端函数

最短总结

如果目标是搞 WayLog 的 Agent,不需要一开始成为 Expo 或 React Native 专家。

最应该先打通的是:

TypeScript 类型
-> 函数和对象
-> 异步请求和存储
-> React 页面状态
-> AgentProposal
-> Trip 本地优先写入

读代码时始终抓住一句话:

Agent 不是直接执行者,它先提出结构化修改;真正写入必须经过校验、预览和确认。

Comments

留下评论

GitHub Issues