TypeScript编写Firebase可调用函数入参类型校验最佳实践
问题根因
TypeScript的interface、type定义仅在编译阶段生效,代码编译为JavaScript后所有类型标注会被完全擦除,运行时根本不存在questionnaireDoc这个值,自然无法通过instanceof做类型判断。
客户端侧的HttpsCallable<parameterType, returnType>仅能约束本地TypeScript代码的编译检查,对实际运行时传入的参数没有任何校验能力:非TS客户端、恶意构造的请求、客户端逻辑bug传错参数,都会直接透传到云函数逻辑中,因此服务端侧的运行时校验是强制要求,不能省略。
你目前用switch判断字段是否为undefined的方案有两个明显缺陷:一是只能判断字段存在性,无法校验字段类型、格式是否符合要求(比如本该传数字的字段传了字符串,会直接绕过校验);二是字段多了之后维护成本极高,新增字段很容易漏加校验。
最佳实践:使用Schema-first的运行时校验库
目前TypeScript生态下最适配这类场景的方案是使用「Schema定义同时生成TS类型」的运行时校验库,一次定义就能同时拿到编译时类型提示和运行时校验能力,不需要重复写类型和校验逻辑,社区最常用的是Zod,和Firebase云函数适配成本极低。
具体实现
- 安装依赖
npm install zod - 定义校验Schema,自动推导TS类型
替换原来手动写的questionnaireDoc接口,用Zod定义字段规则,通过内置的infer方法直接导出对应TS类型,后续改字段规则只需要改Schema一处:import { z } from "zod"; import { https } from "firebase-functions"; // 按你实际使用的v1/v2版本调整导入路径 // 在这里定义每个字段的校验规则:类型、是否必填、格式要求、取值范围等 const QuestionnaireDocSchema = z.object({ property1: z.string().min(1, "property1不能为空字符串"), property2: z.number().int().positive("property2必须为正整数"), property3: z.boolean(), property4: z.string().optional(), // 可选字段 // property5 ~ property9按实际业务规则补充即可 }); // 自动推导TS类型,和你之前手动写的interface效果完全一致 type QuestionnaireDoc = z.infer<typeof QuestionnaireDocSchema>; // 返回值类型也可以用同样的方式定义 const ApiOutSchema = z.object({ code: z.number(), msg: z.string() }); type ApiOut = z.infer<typeof ApiOutSchema>; - 在云函数入口统一做入参校验
用Schema的safeParse方法校验入参,校验失败直接抛出Firebase规范的invalid-argument错误,校验通过后拿到的参数TS会自动识别为正确类型,不需要额外写类型断言:export const questionnaireSubmit = https.onCall(async (data): Promise<ApiOut> => { const parseResult = QuestionnaireDocSchema.safeParse(data); if (!parseResult.success) { // 可以把具体的校验错误信息返回给客户端,方便定位问题 throw new https.HttpsError( "invalid-argument", "请求参数不合法", parseResult.error.issues ); } // 后续业务逻辑直接用parseResult.data即可,类型完全匹配 const { property1, property2 } = parseResult.data; // 正常写业务逻辑 return { code: 0, msg: "提交成功" }; });
方案优势
- 没有重复代码:Schema是唯一数据源,修改字段规则后TS类型自动同步,不会出现类型定义和校验逻辑不一致的问题
- 校验能力完整:不止能判断字段是否存在,还能校验字段类型、格式(比如邮箱、手机号正则)、取值范围、枚举值、嵌套对象/数组结构等
- 可扩展性强:不管有多少字段,只需要在Schema里加对应规则即可,不会出现冗余的重复判断代码
- 错误信息清晰:校验失败时会返回具体哪个字段、因为什么规则不通过,比笼统的“缺少必填字段”更容易排查问题
轻量替代方案(无额外依赖)
如果场景非常简单,不想引入第三方依赖,可以自己写TS类型守卫实现校验,适合字段数量少、规则简单的场景:
function isQuestionnaireDoc(data: unknown): data is QuestionnaireDoc { if (typeof data !== "object" || data === null) return false; const record = data as Record<string, unknown>; return ( typeof record.property1 === "string" && typeof record.property2 === "number" && typeof record.property3 === "boolean" // 补充其余字段的校验规则 ); } // 函数内使用 if (!isQuestionnaireDoc(data)) { throw new https.HttpsError("invalid-argument", "请求参数不合法"); }
注意不要用as类型断言跳过校验,也不要只判断字段存在性忽略类型校验,否则类型不匹配的问题会留到业务逻辑运行时才触发,排查成本极高。
内容的提问来源于stack exchange,提问作者Matthew Keller
相关产品推荐
相关产品推荐

