Skip to content

第一章:语言桥接 -- TypeScript for Java Developers

"学习一门新语言,不是学习语法,而是学习一种思维方式。"


1.1 为什么是 TypeScript?

在 Java 的世界里,你习惯了强类型、接口、泛型、注解。TypeScript 在这些方面与 Java 惊人地相似——它本质上是 JavaScript 加上了 Java 级别的类型系统。

但有几个根本性的差异,如果不先理解它们,读源码时会反复困惑。

1.2 类型系统:结构化 vs 名义化

这是 Java 和 TypeScript 之间最重要的区别。

Java 是名义化(Nominal)类型系统:两个类即使字段完全相同,只要类名不同,就是不同类型。

java
// Java: 这是两个不同的类型
class Point { int x; int y; }
class Coordinate { int x; int y; }
// p1 = new Coordinate(); // 编译错误!类型不兼容

TypeScript 是结构化(Structural)类型系统:只要结构(字段和方法)相同,就是兼容的。

typescript
// 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 中,只要你的对象有 namedescriptionparametersexecute 这些字段,它就自动满足 AgentTool 接口。这就是所谓的"鸭子类型"(duck typing)——如果它走起来像鸭子,叫起来像鸭子,那它就是鸭子。 ─────────────────────────────────────────────────

1.3 联合类型与判别式联合

Java 17 引入了 sealed class,TypeScript 早已有了等价物——联合类型(Union Types)

Java 的做法

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
// 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 对照 ─────────────────────────────────────

JavaTypeScriptPi 中的实例
sealed interfacetype A = B | C | Dtype Message = UserMessage | AssistantMessage | ToolResultMessage
record + implementsinterface + 字面量 role 字段每个 Message 都有 role: "user"
instanceof 检查if (msg.role === "assistant")通过 role 字段判别
switch + 模式匹配switch (event.type)AgentLoop 中的事件处理

─────────────────────────────────────────────────

判别式联合的模式匹配

在 Pi 的源码中,你会反复看到这种模式:

typescript
// 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
// TypeScript
interface AgentTool<TParameters extends TSchema> {
    name: string;
    parameters: TParameters;
    execute: (params: Static<TParameters>) => Promise<AgentToolResult>;
}
java
// Java 等价
interface AgentTool<P extends Schema> {
    String getName();
    P getParameters();
    ToolResult execute(Static<P> params) throws Exception;
}

但 TypeScript 有一个 Java 没有的强大特性——条件类型映射类型

typescript
// 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 中都大量使用:

typescript
// 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&lt;AgentState&gt;;                     // 工具类型

经验法则(Pi 项目遵循的约定):

  • 定义对象形状 → 用 interface
  • 定义联合类型 → 用 type
  • 定义函数签名 → 用 type

1.6 async/await:比 CompletableFuture 更直白

Java 8 的 CompletableFuture 和 TypeScript 的 Promise 解决的是同一个问题——异步编程。但语法差异很大:

java
// Java: CompletableFuture
CompletableFuture&lt;String&gt; future = CompletableFuture
    .supplyAsync(() -> fetchFromLLM())
    .thenApply(response -> parseResponse(response))
    .thenCompose(parsed -> executeTool(parsed))
    .exceptionally(ex -> handleError(ex));

// 阻塞等待
String result = future.join();
typescript
// 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 类非常相似:

typescript
// packages/agent/src/agent.ts(简化)
export class Agent {
    private _state: MutableAgentState;                    // private 字段
    private readonly listeners = new Set&lt;Listener&gt;();     // 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 对照 ─────────────────────────────────────

TypeScriptJava说明
private _stateprivate MutableState state加下划线是 TS 约俗,因为 JS 历史上没有真正的 private
readonlyfinal只读引用
?: string@Nullable String可选字段
??Optional.orElse()||null/undefined 合并
() => void 返回值RunnableSubscription闭包作为取消句柄
Set<Listener>Set<Listener>完全一样

─────────────────────────────────────────────────

1.8 解构赋值与展开运算符

这两个语法在 Pi 中无处不在:

typescript
// 解构赋值 - 类似 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

typescript
// 导出 - 类似 Java 的 public class/interface
export class Agent { ... }
export interface AgentTool&lt;T&gt; { ... }
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 对照 ─────────────────────────────────────

TypeScriptJava
export class Agentpublic class Agent
export default function无直接等价(每个模块一个默认导出)
import type { X }无直接等价(纯类型导入,不产生运行时代码)
@mariozechner/pi-agent-coreMaven 坐标 com.mariozechner:pi-agent-core
package.jsonexportsmodule-info.javaexports

─────────────────────────────────────────────────

1.10 类型守卫与类型断言

当你处理联合类型时,经常需要"收窄"到具体类型:

typescript
// 类型守卫 - 类似 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)。 这是一个非常好的实践。项目通过判别式联合(roletype 字段)和类型守卫函数来实现类型安全的分支处理。如果你在读源码时看到 as,那通常是因为要和某个不够类型安全的第三方库交互。 ─────────────────────────────────────────────────

1.11 函数类型与闭包

TypeScript 的函数是一等公民,可以作为参数传递、作为返回值。这在 Java 中需要 @FunctionalInterface 或 lambda:

typescript
// TypeScript 函数类型
type StreamFn = (
    model: Model<any>,
    context: Context,
    options?: SimpleStreamOptions,
) => AssistantMessageEventStream;

// 作为参数传递 - 类似 Java 的 Function&lt;T, R&gt;
interface AgentOptions {
    convertToLlm?: (messages: AgentMessage[]) => Message[];
    beforeToolCall?: (context: BeforeToolCallContext) => Promise&lt;BeforeToolCallResult&gt;;
}

// 闭包 - 类似 Java lambda 捕获外部变量
function createTool(cwd: string): AgentTool {
    return {
        name: "read",
        execute: async (id, params) => {    // 闭包捕获了 cwd
            const fullPath = path.join(cwd, params.path);
            // ...
        }
    };
}

⚡ Java 对照 ─────────────────────────────────────

TypeScriptJava
(x: number) => stringFunction<Integer, String>
(msg: Message) => voidConsumer<Message>
() => voidRunnable
(ctx: Context) => Promise<Result>Function<Context, CompletableFuture<Result>>
闭包自动捕获lambda 的 effectively final

─────────────────────────────────────────────────

1.12 类型工具:Partial, Required, Pick, Omit

TypeScript 提供了内置的类型变换工具,Pi 中频繁使用:

typescript
// Partial&lt;T&gt; - 所有字段变为可选(类似 Builder 模式的中间状态)
interface AgentState {
    systemPrompt: string;
    model: Model<any>;
    isStreaming: boolean;
}
type PartialState = Partial&lt;AgentState&gt;;
// { systemPrompt?: string; model?: Model<any>; isStreaming?: boolean; }

// Pick&lt;T, K&gt; - 只取指定字段(类似投影查询)
type ReadonlySessionManager = Pick&lt;SessionManager,
    'getCwd' | 'getSessionId' | 'getEntry' | 'getBranch'
&gt;;

// Omit&lt;T, K&gt; - 排除指定字段
type MutableAgentState = Omit&lt;AgentState, 'isStreaming' | 'pendingToolCalls'&gt; & {
    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 的核心差异,我们可以开始读源码了。

目录 | 第二章:项目全景 -- Monorepo 结构与构建系统 →

基于 MIT 许可证发布