欢迎光临
我们一直在努力

TypeScript 类型体操实战:为 AI SDK 写一套类型安全的 API 封装

TypeScript 类型体操实战:为 AI SDK 写一套类型安全的 API 封装

文章总体概览信息图

前言

每次调用大模型 API,都要手写一堆 as any。返回值类型不确定,参数拼写错了编译器也不报错。

受够了。花了一个周末,用 TypeScript 的高级类型给 OpenAI SDK 套了一层类型安全的封装。现在写错参数名,IDE 直接标红。

代码不多,但类型体操的含量不低。

一、问题:没有类型的日子

1.1 典型的"裸调"代码

// ❌ 没有类型约束的调用方式
const response = await fetch('/api/chat', {
method: 'POST',
body: JSON.stringify({
modle: 'gpt-4o', // 拼写错了,编译器不报错
mesages: [], // 又拼错了
temprature: 0.7, // 还是拼错了
}),
});

const data = await response.json(); // any 类型,啥都不知道
console.log(data.chocies[0].message); // 运行时才爆炸

三个拼写错误,TypeScript 一个没拦住。因为 body 的类型就是 string。

1.2 我们需要什么

graph LR
A["开发者写代码"] –> B{"TypeScript 编译器"}
B –>|参数拼错| C["❌ 编译报错"]
B –>|缺少必填参数| D["❌ 编译报错"]
B –>|返回值类型推断| E["✅ 自动补全"]

style C fill:#ef4444,color:#fff
style D fill:#ef4444,color:#fff
style E fill:#10b981,color:#fff

二、基础类型定义

2.1 模型与消息类型

// types/ai.ts — AI SDK 基础类型

/** 支持的模型列表(字面量类型,拼错直接报错) */
type ModelName =
| 'gpt-4o'
| 'gpt-4o-mini'
| 'gpt-4-turbo'
| 'claude-3-5-sonnet'
| 'claude-3-5-haiku';

/** 消息角色 */
type Role = 'system' | 'user' | 'assistant';

/** 单条消息 */
interface Message {
role: Role;
content: string;
}

/** 请求参数 */
interface ChatRequest {
model: ModelName; // 拼错模型名直接报错
messages: Message[]; // 消息数组
temperature?: number; // 可选,0-2
max_tokens?: number; // 可选
stream?: boolean; // 是否流式
}

/** 非流式响应 */
interface ChatResponse {
id: string;
model: string;
choices: {
index: number;
message: Message;
finish_reason: 'stop' | 'length' | 'content_filter';
}[];
usage: {
prompt_tokens: number;
completion_tokens: number;
total_tokens: number;
};
}

现在拼错模型名,TypeScript 直接标红:

// ✅ 正确
const req: ChatRequest = { model: 'gpt-4o', messages: [] };

// ❌ 编译错误:类型 '"gpt-4" ' 不能分配给类型 'ModelName'
const req2: ChatRequest = { model: 'gpt-4', messages: [] };

三、高级类型体操

3.1 条件类型:根据 stream 参数推断返回类型

这是最精彩的部分。当 stream: true 时返回 ReadableStream,否则返回 ChatResponse。

// 核心类型体操:条件返回类型
type ChatResult<T extends ChatRequest> =
T extends { stream: true }
? ReadableStream<Uint8Array> // 流式返回 Stream
: ChatResponse; // 非流式返回完整响应

// 验证效果:
type 测试流式 = ChatResult<{ model: 'gpt-4o'; messages: []; stream: true }>;
// 推断为 ReadableStream<Uint8Array> ✅

type 测试非流式 = ChatResult<{ model: 'gpt-4o'; messages: [] }>;
// 推断为 ChatResponse ✅

3.2 函数重载实现

// AI 客户端类
class AIClient {
private baseUrl: string;
private apiKey: string;

constructor(config: { baseUrl: string; apiKey: string }) {
this.baseUrl = config.baseUrl;
this.apiKey = config.apiKey;
}

// 函数重载声明:编译器根据 stream 参数推断返回类型
async chat(req: ChatRequest & { stream: true }): Promise<ReadableStream>;
async chat(req: ChatRequest & { stream?: false }): Promise<ChatResponse>;
async chat(req: ChatRequest): Promise<ReadableStream | ChatResponse> {
const response = await fetch(`${this.baseUrl}/chat/completions`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${this.apiKey}`,
},
body: JSON.stringify(req),
});

if (!response.ok) {
const error = await response.text();
throw new Error(`AI 请求失败 (${response.status}): ${error}`);
}

// 流式:直接返回 body stream
if (req.stream) {
if (!response.body) throw new Error('响应体为空');
return response.body;
}

// 非流式:解析 JSON
return response.json() as Promise<ChatResponse>;
}
}

使用效果:

const client = new AIClient({
baseUrl: 'https://api.openai.com/v1',
apiKey: process.env.OPENAI_API_KEY!,
});

// 非流式调用 → 自动推断为 ChatResponse
const result = await client.chat({
model: 'gpt-4o-mini',
messages: [{ role: 'user', content: '你好' }],
});
console.log(result.choices[0].message.content); // 有类型提示 ✅

// 流式调用 → 自动推断为 ReadableStream
const stream = await client.chat({
model: 'gpt-4o-mini',
messages: [{ role: 'user', content: '你好' }],
stream: true,
});
// stream 是 ReadableStream 类型,IDE 自动补全 getReader() ✅

3.3 工具调用类型安全

/** 工具定义(Function Calling) */
interface ToolDefinition<N extends string, P extends Record<string, unknown>> {
type: 'function';
function: {
name: N;
description: string;
parameters: {
type: 'object';
properties: P;
required?: (keyof P)[];
};
};
}

/** 工具调用结果 */
interface ToolCall<N extends string> {
id: string;
type: 'function';
function: {
name: N;
arguments: string; // JSON 字符串
};
}

// 使用示例:定义一个天气查询工具
type 天气工具 = ToolDefinition<'查询天气', {
城市: { type: 'string'; description: '城市名称' };
日期: { type: 'string'; description: '查询日期' };
}>;

// 工具调用的类型会自动推断
const tool: 天气工具 = {
type: 'function',
function: {
name: '查询天气', // 写错名字直接报错
description: '查询指定城市的天气预报',
parameters: {
type: 'object',
properties: {
城市: { type: 'string', description: '城市名称' },
日期: { type: 'string', description: '查询日期' },
},
required: ['城市'], // 只能填 '城市' 或 '日期',填别的报错
},
},
};

四、实用工具类型

4.1 深度只读

/** 深度只读:防止意外修改 API 响应数据 */
type DeepReadonly<T> = {
readonly [P in keyof T]: T[P] extends object
? T[P] extends Function
? T[P]
: DeepReadonly<T[P]>
: T[P];
};

// 使用
const response: DeepReadonly<ChatResponse> = await client.chat({
model: 'gpt-4o-mini',
messages: [{ role: 'user', content: '你好' }],
});

// ❌ 编译错误:无法分配到 "content",因为它是只读属性
// response.choices[0].message.content = '被篡改';

4.2 参数校验类型

/** 约束 temperature 必须在 0-2 之间(编译期检查) */
type ValidTemperature = number & { __brand: 'temperature' };

function asTemperature(value: number): ValidTemperature {
if (value < 0 || value > 2) {
throw new RangeError(`temperature 必须在 0-2 之间,收到: ${value}`);
}
return value as ValidTemperature;
}

// 使用
const temp = asTemperature(0.7); // ✅
const temp2 = asTemperature(5); // 运行时报错

五、完整的封装示例

// lib/ai-client.ts — 生产级 AI 客户端

export class TypeSafeAI {
private client: AIClient;

constructor(apiKey: string) {
this.client = new AIClient({
baseUrl: 'https://api.openai.com/v1',
apiKey,
});
}

/** 简单对话(非流式) */
async ask(问题: string, model: ModelName = 'gpt-4o-mini'): Promise<string> {
const result = await this.client.chat({
model,
messages: [{ role: 'user', content: 问题 }],
temperature: 0.7,
});

return result.choices[0]?.message.content ?? '';
}

/** 带系统提示词的对话 */
async askWithSystem(
系统提示: string,
用户消息: string,
options?: { model?: ModelName; temperature?: number }
): Promise<string> {
const result = await this.client.chat({
model: options?.model ?? 'gpt-4o-mini',
messages: [
{ role: 'system', content: 系统提示 },
{ role: 'user', content: 用户消息 },
],
temperature: options?.temperature ?? 0.7,
});

return result.choices[0]?.message.content ?? '';
}

/** 流式对话 */
async stream(问题: string): Promise<ReadableStream> {
return this.client.chat({
model: 'gpt-4o-mini',
messages: [{ role: 'user', content: 问题 }],
stream: true,
});
}
}

// 使用示例
const ai = new TypeSafeAI(process.env.OPENAI_API_KEY!);

const 回答 = await ai.ask('TypeScript 的类型体操有什么用?');
console.log(回答); // string 类型,有自动补全

六、总结

TypeScript 类型体操不是炫技。在 AI 开发场景下,它能实实在在地:

  • 防止拼写错误:模型名、参数名写错编译器直接拦住
  • 自动推断返回类型:stream=true 返回 Stream,否则返回 JSON
  • IDE 全程补全:写代码时不用翻文档
  • 好的类型,是不需要写注释的注释。代码本身就是最好的文档。

    赞(0)
    未经允许不得转载:171主机测评 » TypeScript 类型体操实战:为 AI SDK 写一套类型安全的 API 封装
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址