流式服务端渲染(Streaming SSR)的核心思想是把原本一次性返回的完整HTML拆成多个分片,服务端准备好哪一块就先推送哪一块,浏览器收到后立即渲染。React的服务端组件(RSC)进一步把这套思路应用到了组件粒度的数据传输上。但这对类型系统提出了新要求:数据不再是静态的、完整的,而是带有生命周期状态的。TypeScript本身没有内置“流式数据”这种类型,我们需要自己动手建模。本文围绕这一主题展开,从状态建模、类型设计到序列化边界处理,给出一套完整的方案。

为什么流式数据需要专门的类型建模
传统的接口类型定义假设数据是“一次性到达”的:请求发出,响应返回,拿到的要么是完整对象,要么是错误。而流式场景打破了这一假设。同一条逻辑数据在不同时刻可能处于完全不同的状态:尚未开始传输、传输中、传输完成、传输失败。如果把所有状态都塞进一个可选字段满天飞的大对象里,TypeScript的检查能力就形同虚设。
一个典型的错误做法是这样的:
interface BadStreamData<T> {
data?: T;
error?: string;
loading?: boolean;
}这种定义的问题是它允许{ data: undefined, error: undefined, loading: false }这种逻辑上非法的状态组合。编译器不会阻止你同时设置data和error,也不会强制你在loading为true时访问data前做空值检查。类型系统应该让非法状态不可表示(make illegal states unrepresentable),而不是靠运行时判断和自觉。
用可辨识联合建模数据生命周期
正确的方式是使用可辨识联合(discriminated union),为每个阶段定义一个独立的类型,通过一个公共的字面量字段作为标签区分。这样TypeScript就能在switch或if判断后自动收窄类型:
type StreamPhase = 'pending' | 'success' | 'error';
interface PendingChunk<T> {
status: 'pending';
id: string;
}
interface SuccessChunk<T> {
status: 'success';
id: string;
data: T;
/** 流式分片的序号,客户端按序拼接 */
index: number;
}
interface ErrorChunk {
status: 'error';
id: string;
message: string;
/** 错误码,便于客户端统一处理 */
code: number;
}
type StreamChunk<T> = PendingChunk<T> | SuccessChunk<T> | ErrorChunk;这样定义之后,访问chunk.data之前必须先确认chunk.status === 'success',否则编译直接报错。非法状态组合被类型层面彻底排除。注意PendingChunk上的泛型参数T虽然没被使用,但保留它可以让StreamChunk<User>这样的整体类型保持一致,避免调用方做额外的类型断言。
在服务端组件的渲染函数中消费这个类型时,收窄效果非常自然:
function renderChunk(chunk: StreamChunk<User>): string {
switch (chunk.status) {
case 'pending':
return '<div class="skeleton">加载中</div>';
case 'success':
// 此处 chunk.data 已被收窄为 User 类型
return `<div>${chunk.data.name}</div>`;
case 'error':
return `<div>出错了:${chunk.message}</div>`;
}
}封装通用的流式响应类型并处理序列化边界
服务端组件传输的数据最终要经过序列化变成字节流。TypeScript里有些类型在序列化时会丢失信息,最典型的是Date会变成字符串、Map会变成空对象。如果服务端的类型定义直接使用这些类型,客户端反序列化后的实际结构和类型声明就不一致了。解决方法是定义一个“序列化后”的映射类型,在传输边界做一次转换:
type Serialized<T> = {
[K in keyof T]: T[K] extends Date ? string
: T[K] extends Map<infer U, infer V>? Array<[U, V]>
: T[K] extends object ? Serialized<T[K]>
: T[K];
};
/** 最终的传输载荷类型:流式分片 + 序列化数据 */
type TransportChunk<T> = StreamChunk<Serialized<T>>;有了TransportChunk,服务端在写出分片时数据已经被约束为可安全序列化的形态,客户端拿到的类型与服务端实际发送的内容严格一致。对于包含函数、类实例等真正无法序列化的成员,建议在定义业务实体时就拆分成可传输的数据部分和留在服务端的行为部分,而不是依赖序列化器静默丢弃。
此外还可以补充一些工程化的辅助类型。比如用Awaited<T>处理异步组件的返回值,用Readonly<T>防止客户端意外修改分片数据,用Partial<T>配合增量更新场景表达“本次分片只携带变化的字段”。这些工具类型组合起来,就能在服务端组件与客户端之间建立一份类型安全的传输契约,让流式渲染的每一步都有编译器兜底,而不是靠人肉记忆数据处于哪个阶段。
TypeScript服务端组件流式传输修改时间:2026-09-12 12:10:32