问题场景
你在写一个用户信息获取接口:
typescript
interface User {
id: number
name: string
email: string
avatar: string
age: number
}
// API 返回数据
const raw = localStorage.getItem('user_profile')
const user: User = JSON.parse(raw!) // ❌ TS 无报错,但运行时崩了
console.log(user.name.toUpperCase()) // 💥 Cannot read properties of null
TS 类型检查通过,代码跑起来就炸。为什么?
因为 JSON.parse 的返回类型是 any 。any 可以赋值给任何类型,TS 编译器认为这是合法的。但你解析出来的是啥?可能是 null、undefined、{}、甚至数组------全看后端 / 本地存储给什么。
这就是 TS 最大的幻觉 :类型标注 ≠ 类型保障。as 断言只是告诉编译器"相信我",但运行时没有任何校验。
原因分析
| 阶段 | 做了什么 | 是否安全 |
|---|---|---|
| 编译时 | TS 看到 JSON.parse(raw!) 返回 any,赋值给 User OK |
✅ |
| 运行时 | JSON.parse 真的解析了一段 JSON |
❌ 不校验结构 |
| 运行时 | 访问 user.name.toUpperCase() |
💥 user 可能是 null |
核心矛盾:TS 只在编译期生效,JSON.parse 发生在运行期。两者之间没有任何桥梁。
常见的翻车场景:
- localStorage 被用户手动篡改了
- 本地缓存格式版本不匹配(旧版本存的是
{name: string},新版本要{firstName: string}) - API 字段变了但前端忘了同步类型
- 某个字段返回了
null但 TS 类型写了string
解决方案
方案一:手写类型守卫(最朴素)
typescript
function isUser(data: unknown): data is User {
if (!data || typeof data !== 'object') return false
const d = data as Record<string, unknown>
return (
typeof d.id === 'number' &&
typeof d.name === 'string' &&
typeof d.email === 'string' &&
typeof d.avatar === 'string' &&
typeof d.age === 'number'
)
}
const raw = localStorage.getItem('user_profile')
const parsed: unknown = JSON.parse(raw ?? 'null')
if (!isUser(parsed)) {
throw new Error('用户数据格式异常,请重新登录')
}
// 此时 parsed 已被收窄为 User
console.log(parsed.name.toUpperCase()) // ✅ 安全
优点:零依赖,逻辑清晰 缺点:字段多了写死人,容易漏字段
方案二:Zod 运行时校验(推荐生产使用)
Zod 是当前最流行的运行时校验库,让你写一次 Schema 同时得到 TS 类型 + 运行时校验:
typescript
import { z } from 'zod'
// 定义 Schema --- 同时生成 TS 类型
const UserSchema = z.object({
id: z.number(),
name: z.string(),
email: z.string().email(),
avatar: z.string().url().default('/default-avatar.png'),
age: z.number().min(0).max(150).optional(),
})
// 👇 从 Schema 推导出 TS 类型,保持一致
type User = z.infer<typeof UserSchema>
// 等价于:
// { id: number; name: string; email: string; avatar: string; age?: number }
// 解析数据
const raw = localStorage.getItem('user_profile')
const result = UserSchema.safeParse(JSON.parse(raw ?? 'null'))
if (!result.success) {
console.error('数据校验失败:', result.error.issues)
// issues 详细告诉你哪个字段不对
// [{ code: 'invalid_type', path: ['email'], message: 'Expected string, received null' }]
return
}
// result.data 已经是完整的 User 类型,且 avatar 有默认值
const user: User = result.data
Zod 的优势:
| 能力 | Zod | 手写守卫 |
|---|---|---|
| TS 类型推导 | ✅ z.infer 自动推导 |
❌ 手动定义 |
| 字段级错误信息 | ✅ | ❌ 只能整体返回 |
| 默认值处理 | ✅ .default() |
❌ 自己写合并 |
| 嵌套对象/数组 | ✅ 原生支持 | 😵 递归写到崩溃 |
| 数据转换 | ✅ .transform() |
❌ |
更多 Zod 实用场景:
typescript
// 解析 API 响应 ------ 后端说好的一定给,实际可能没有
const ApiResponseSchema = z.object({
code: z.number(),
data: z.unknown(),
message: z.string().optional(),
})
// 解析复杂嵌套
const ConfigSchema = z.object({
plugins: z.array(z.object({
name: z.string(),
enabled: z.boolean().default(true),
options: z.record(z.unknown()).optional(),
})),
version: z.string().regex(/^\d+.\d+.\d+$/),
})
// .parse() 直接抛异常 vs .safeParse() 返回 Result 类型
// 前端建议用 safeParse,优雅处理错误
方案三:Valibot ------ Zod 的轻量替代
Valibot 是 2023 年底出现的新秀,核心优势:Tree-shakable,按需引入后体积只有 Zod 的 1/10:
typescript
import { object, string, number, email, minLength, safeParse } from 'valibot'
const UserSchema = object({
id: number(),
name: string([minLength(1)]),
email: string([email()]),
age: number(),
})
const result = safeParse(UserSchema, JSON.parse(raw ?? 'null'))
如果你的项目对包体积敏感(比如组件库、SDK),Valibot 更合适。
实操代码:封装通用 JSON 安全解析函数
typescript
import { z } from 'zod'
/**
* 安全解析 JSON + Schema 校验
* @param jsonStr JSON 字符串(可能为 null/undefined/非法格式)
* @param schema Zod Schema
* @param fallback 解析失败时返回的默认值
*/
function safeJsonParse<T extends z.ZodTypeAny>(
jsonStr: string | null | undefined,
schema: T,
fallback?: z.infer<T>
): z.infer<T> {
if (!jsonStr) {
if (fallback !== undefined) return fallback
throw new Error('JSON 字符串为空')
}
let parsed: unknown
try {
parsed = JSON.parse(jsonStr)
} catch {
if (fallback !== undefined) return fallback
throw new Error('JSON 解析失败')
}
const result = schema.safeParse(parsed)
if (!result.success) {
console.warn('Schema 校验失败,使用默认值', result.error.issues)
if (fallback !== undefined) return fallback
throw new Error(`数据格式异常: ${result.error.issues.map(i => i.path.join('.') + ': ' + i.message).join('; ')}`)
}
return result.data
}
// 使用示例
const userSchema = z.object({
id: z.number(),
name: z.string(),
email: z.string().email(),
})
// 哪怕 localStorage 被清空,也不会崩溃
const user = safeJsonParse(
localStorage.getItem('user_profile'),
userSchema,
{ id: 0, name: 'Guest', email: 'guest@example.com' }
)
要点总结
JSON.parse返回any,any赋值给任何类型 TS 都放行------这是运行时崩溃的根源as类型断言 ≠ 数据校验,它只是让编译器闭嘴- Zod 的
z.infer解决了"写两遍类型"的问题,Schema 即类型源 - 所有"外部输入"都需要运行时校验:localStorage、API 响应、URL 参数、用户输入
- 非核心场景用 Zod 的
safeParse+ 默认值兜底,核心场景(支付、身份认证)必须严格校验 - 包体积敏感用 Valibot,否则 Zod 生态更成熟(还有 Zod 的 OpenAPI 生成器等插件)
一条铁律:不要相信任何从
JSON.parse出来的数据。TS 类型只是合同,Zod 才是保安。