第一章:语言桥接 -- TypeScript for Java Developers
"学习一门新语言,不是学习语法,而是学习一种思维方式。"
1.1 为什么是 TypeScript?
在 Java 的世界里,你习惯了强类型、接口、泛型、注解。TypeScript 在这些方面与 Java 惊人地相似——它本质上是 JavaScript 加上了 Java 级别的类型系统。
但有几个根本性的差异,如果不先理解它们,读源码时会反复困惑。
1.2 类型系统:结构化 vs 名义化
这是 Java 和 TypeScript 之间最重要的区别。
Java 是名义化(Nominal)类型系统:两个类即使字段完全相同,只要类名不同,就是不同类型。
// Java: 这是两个不同的类型
class Point { int x; int y; }
class Coordinate { int x; int y; }
// p1 = new Coordinate(); // 编译错误!类型不兼容TypeScript 是结构化(Structural)类型系统:只要结构(字段和方法)相同,就是兼容的。
// TypeScript: 这两个类型是兼容的
interface Point { x: number; y: number }
interface Coordinate { x: number; y: number }
const p: Point = { x: 1, y: 2 } // OK
const c: Coordinate = p // OK! 结构相同就兼容★ Insight ─────────────────────────────────────这解释了为什么 Pi 的源码中很少看到 implements 关键字。 在 Java 中你需要 class MyTool implements AgentTool,但在 TypeScript 中,只要你的对象有 name、description、parameters、execute 这些字段,它就自动满足 AgentTool 接口。这就是所谓的"鸭子类型"(duck typing)——如果它走起来像鸭子,叫起来像鸭子,那它就是鸭子。 ─────────────────────────────────────────────────
1.3 联合类型与判别式联合
Java 17 引入了 sealed class,TypeScript 早已有了等价物——联合类型(Union Types)。
Java 的做法
// Java 17+
sealed interface Message permits UserMessage, AssistantMessage, ToolResultMessage {}
record UserMessage(String content, long timestamp) implements Message {}
record AssistantMessage(List<Content> content, Usage usage) implements Message {}
record ToolResultMessage(String toolCallId, List<Content> content, boolean isError) implements Message {}TypeScript 的做法
// TypeScript - Pi 中的实际用法 (packages/ai/src/types.ts)
interface UserMessage {
role: "user"; // 字面量类型,充当判别标签
content: string | (TextContent | ImageContent)[];
timestamp: number;
}
interface AssistantMessage {
role: "assistant"; // 不同的判别标签
content: (TextContent | ThinkingContent | ToolCall)[];
api: Api;
provider: Provider;
model: string;
usage: Usage;
stopReason: StopReason;
timestamp: number;
}
interface ToolResultMessage {
role: "toolResult"; // 又一个判别标签
toolCallId: string;
toolName: string;
content: (TextContent | ImageContent)[];
isError: boolean;
timestamp: number;
}
// 联合类型 = sealed interface
type Message = UserMessage | AssistantMessage | ToolResultMessage;⚡ Java 对照 ─────────────────────────────────────
| Java | TypeScript | Pi 中的实例 |
|---|---|---|
sealed interface | type A = B | C | D | type Message = UserMessage | AssistantMessage | ToolResultMessage |
record + implements | interface + 字面量 role 字段 | 每个 Message 都有 role: "user" 等 |
instanceof 检查 | if (msg.role === "assistant") | 通过 role 字段判别 |
switch + 模式匹配 | switch (event.type) | AgentLoop 中的事件处理 |
─────────────────────────────────────────────────
判别式联合的模式匹配
在 Pi 的源码中,你会反复看到这种模式:
// agent-loop.ts 中的事件处理
switch (event.type) {
case "start":
partialMessage = event.partial; // TypeScript 知道这里 event 是 StartEvent
break;
case "text_delta":
// 这里 event 自动收窄为 TextDeltaEvent
// 可以安全访问 event.delta
break;
case "done":
// 这里 event 是 DoneEvent
break;
}TypeScript 的类型收窄(narrowing)机制确保你在每个 case 分支中都能安全访问该分支特有的字段,无需强制转换。这比 Java 的 instanceof + 强制转换优雅得多。
1.4 泛型:几乎一样,但更灵活
TypeScript 的泛型和 Java 非常相似:
// TypeScript
interface AgentTool<TParameters extends TSchema> {
name: string;
parameters: TParameters;
execute: (params: Static<TParameters>) => Promise<AgentToolResult>;
}// Java 等价
interface AgentTool<P extends Schema> {
String getName();
P getParameters();
ToolResult execute(Static<P> params) throws Exception;
}但 TypeScript 有一个 Java 没有的强大特性——条件类型和映射类型:
// Pi 中的用法 (packages/ai/src/types.ts)
interface Model<TApi extends Api> {
id: string;
api: TApi;
// 根据 TApi 的不同,compat 字段的类型也不同!
compat?: TApi extends "openai-completions"
? OpenAICompletionsCompat
: TApi extends "openai-responses"
? OpenAIResponsesCompat
: TApi extends "anthropic-messages"
? AnthropicMessagesCompat
: never;
}这在 Java 中需要通过重载或 Visitor 模式才能实现,TypeScript 直接在类型层面完成了。
★ Insight ─────────────────────────────────────Static<T> 是什么? 这是 TypeBox 库的核心函数。TypeBox 让你用代码定义 JSON Schema,而 Static<T> 能从 schema 定义中"提取"出 TypeScript 类型。在 Java 中你可能用过 Jackson 的 @JsonSchema 或 Hibernate Validator 的注解来做类似的事,但 TypeBox 把 schema 定义和类型推断统一到了一个系统里,消除了重复声明。 ─────────────────────────────────────────────────
1.5 接口 vs 类型别名
TypeScript 有两个定义类型的方式,Pi 中都大量使用:
// interface - 可以被 extends 和 implements
interface AgentState {
systemPrompt: string;
model: Model<any>;
isStreaming: boolean;
}
// type - 更灵活,支持联合类型、交叉类型、工具类型
type Message = UserMessage | AssistantMessage | ToolResultMessage; // 联合类型,只能用 type
type AgentEvent = { type: "agent_start" } | { type: "agent_end" }; // 判别式联合
type ReadonlyAgentState = Readonly<AgentState>; // 工具类型经验法则(Pi 项目遵循的约定):
- 定义对象形状 → 用
interface - 定义联合类型 → 用
type - 定义函数签名 → 用
type
1.6 async/await:比 CompletableFuture 更直白
Java 8 的 CompletableFuture 和 TypeScript 的 Promise 解决的是同一个问题——异步编程。但语法差异很大:
// Java: CompletableFuture
CompletableFuture<String> future = CompletableFuture
.supplyAsync(() -> fetchFromLLM())
.thenApply(response -> parseResponse(response))
.thenCompose(parsed -> executeTool(parsed))
.exceptionally(ex -> handleError(ex));
// 阻塞等待
String result = future.join();// TypeScript: async/await
async function runAgent(): Promise<void> {
try {
const response = await fetchFromLLM(); // 等价于 .join()
const parsed = parseResponse(response);
const result = await executeTool(parsed);
} catch (error) { // 等价于 .exceptionally()
handleError(error);
}
}★ Insight ─────────────────────────────────────Pi 中的 Agent Loop 本质上就是一个 while(true) 循环,内部用 await 等待 LLM 响应和工具执行。 如果你理解了 CompletableFuture,你就已经理解了 TypeScript 的 Promise。唯一的区别是 TypeScript 的 await 让异步代码看起来像同步代码,而 Java 直到虚拟线程(Project Loom)才有了类似的体验。 ─────────────────────────────────────────────────
1.7 类:几乎一模一样
Pi 中的 Agent 类和 Java 类非常相似:
// packages/agent/src/agent.ts(简化)
export class Agent {
private _state: MutableAgentState; // private 字段
private readonly listeners = new Set<Listener>(); // readonly 集合
public streamFn: StreamFn; // public 字段
public sessionId?: string; // 可选字段
constructor(options: AgentOptions = {}) { // 构造函数
this._state = createMutableAgentState(options.initialState);
this.streamFn = options.streamFn ?? streamSimple; // ?? 是 null 合并,等价于 Java 的 ||
}
get state(): AgentState { // getter - Java 的 getXxx()
return this._state;
}
subscribe(listener: Listener): () => void { // 返回取消订阅函数
this.listeners.add(listener);
return () => this.listeners.delete(listener); // 闭包!Java 中需要显式 Subscription 对象
}
async prompt(input: string): Promise<void> { // 异步方法
// ...
}
}⚡ Java 对照 ─────────────────────────────────────
| TypeScript | Java | 说明 |
|---|---|---|
private _state | private MutableState state | 加下划线是 TS 约俗,因为 JS 历史上没有真正的 private |
readonly | final | 只读引用 |
?: string | @Nullable String | 可选字段 |
?? | Optional.orElse() 或 || | null/undefined 合并 |
() => void 返回值 | Runnable 或 Subscription | 闭包作为取消句柄 |
Set<Listener> | Set<Listener> | 完全一样 |
─────────────────────────────────────────────────
1.8 解构赋值与展开运算符
这两个语法在 Pi 中无处不在:
// 解构赋值 - 类似 Java 的 var 模式匹配(Java 16+)
const { name, description, parameters } = tool;
// 等价于
const name = tool.name;
const description = tool.description;
const parameters = tool.parameters;
// 展开运算符 - 类似 Java 的 List.copyOf() 或 new ArrayList<>(list)
const newMessages = [...existingMessages, newMessage]; // 不可变追加
const newState = { ...oldState, isStreaming: true }; // 不可变更新
// 函数参数中的展开
function createAgent(options: AgentOptions = {}) { ... } // 默认参数★ Insight ─────────────────────────────────────为什么 Pi 大量使用 [...array, item] 而不是 array.push(item)? 因为不可变性(immutability)。Pi 的 Agent 状态管理遵循函数式编程范式——不直接修改旧状态,而是创建新状态。这和 Java 中 Builder 模式的理念一致,但语法更轻量。在 Agent 类中,set messages() setter 会自动复制数组:messages = nextMessages.slice(),防止外部修改内部状态。 ─────────────────────────────────────────────────
1.9 模块系统:import/export
Java 用 package + import,TypeScript 用 module + import/export:
// 导出 - 类似 Java 的 public class/interface
export class Agent { ... }
export interface AgentTool<T> { ... }
export type Message = UserMessage | AssistantMessage;
// 导入 - 类似 Java 的 import com.xxx.Agent
import { Agent, AgentTool } from "@mariozechner/pi-agent-core";
import type { Message } from "@mariozechner/pi-ai"; // type-only import(编译后删除)⚡ Java 对照 ─────────────────────────────────────
| TypeScript | Java |
|---|---|
export class Agent | public class Agent |
export default function | 无直接等价(每个模块一个默认导出) |
import type { X } | 无直接等价(纯类型导入,不产生运行时代码) |
@mariozechner/pi-agent-core | Maven 坐标 com.mariozechner:pi-agent-core |
package.json 的 exports | module-info.java 的 exports |
─────────────────────────────────────────────────
1.10 类型守卫与类型断言
当你处理联合类型时,经常需要"收窄"到具体类型:
// 类型守卫 - 类似 Java 的 instanceof 检查
function processMessage(msg: Message) {
if (msg.role === "assistant") {
// TypeScript 自动将 msg 收窄为 AssistantMessage
console.log(msg.model); // OK,AssistantMessage 有 model 字段
console.log(msg.stopReason); // OK
}
}
// 自定义类型守卫 - 类似 Java 的 instanceof 但更灵活
function isError(msg: Message): msg is ToolResultMessage {
return msg.role === "toolResult" && msg.isError;
}
// 类型断言 - 类似 Java 的强制转换 (AssistantMessage) msg
const model = (msg as AssistantMessage).model;★ Insight ─────────────────────────────────────Pi 几乎不使用类型断言(as)。 这是一个非常好的实践。项目通过判别式联合(role、type 字段)和类型守卫函数来实现类型安全的分支处理。如果你在读源码时看到 as,那通常是因为要和某个不够类型安全的第三方库交互。 ─────────────────────────────────────────────────
1.11 函数类型与闭包
TypeScript 的函数是一等公民,可以作为参数传递、作为返回值。这在 Java 中需要 @FunctionalInterface 或 lambda:
// TypeScript 函数类型
type StreamFn = (
model: Model<any>,
context: Context,
options?: SimpleStreamOptions,
) => AssistantMessageEventStream;
// 作为参数传递 - 类似 Java 的 Function<T, R>
interface AgentOptions {
convertToLlm?: (messages: AgentMessage[]) => Message[];
beforeToolCall?: (context: BeforeToolCallContext) => Promise<BeforeToolCallResult>;
}
// 闭包 - 类似 Java lambda 捕获外部变量
function createTool(cwd: string): AgentTool {
return {
name: "read",
execute: async (id, params) => { // 闭包捕获了 cwd
const fullPath = path.join(cwd, params.path);
// ...
}
};
}⚡ Java 对照 ─────────────────────────────────────
| TypeScript | Java |
|---|---|
(x: number) => string | Function<Integer, String> |
(msg: Message) => void | Consumer<Message> |
() => void | Runnable |
(ctx: Context) => Promise<Result> | Function<Context, CompletableFuture<Result>> |
| 闭包自动捕获 | lambda 的 effectively final |
─────────────────────────────────────────────────
1.12 类型工具:Partial, Required, Pick, Omit
TypeScript 提供了内置的类型变换工具,Pi 中频繁使用:
// Partial<T> - 所有字段变为可选(类似 Builder 模式的中间状态)
interface AgentState {
systemPrompt: string;
model: Model<any>;
isStreaming: boolean;
}
type PartialState = Partial<AgentState>;
// { systemPrompt?: string; model?: Model<any>; isStreaming?: boolean; }
// Pick<T, K> - 只取指定字段(类似投影查询)
type ReadonlySessionManager = Pick<SessionManager,
'getCwd' | 'getSessionId' | 'getEntry' | 'getBranch'
>;
// Omit<T, K> - 排除指定字段
type MutableAgentState = Omit<AgentState, 'isStreaming' | 'pendingToolCalls'> & {
isStreaming: boolean; // 从 readonly 变为 mutable
pendingToolCalls: Set<string>;
};
// satisfies - 类型检查但不改变推断类型(Pi 中常用)
const DEFAULT_MODEL = {
id: "unknown",
name: "unknown",
api: "unknown",
provider: "unknown",
baseUrl: "",
reasoning: false,
input: [],
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
contextWindow: 0,
maxTokens: 0,
} satisfies Model<any>; // 检查是否满足 Model<any>,但保留字面量类型1.13 本章小结
| 你需要知道的 | 核心要点 |
|---|---|
| 结构化类型 | 看结构,不看名字 |
| 联合类型 | type A = B | C = Java 的 sealed interface |
| 判别式联合 | 用 type/role 字段做模式匹配 |
| async/await | 就是 CompletableFuture.join() 的语法糖 |
| 解构/展开 | const {a, b} = obj 和 [...arr, item] |
| 模块导入 | import { X } from "包名" |
| 类型守卫 | if (x.type === "foo") 自动收窄 |
| 函数类型 | (参数) => 返回值 = Java 的函数式接口 |
下一步:现在你已经掌握了 TypeScript 的核心差异,我们可以开始读源码了。