深夜加班排查bug,发现一个奇怪的问题:TypeScript明明显示类型正确,但用户看到的价格却显示成NaN。查看日志才发现,有个客户端发送的商品价格是字符串"99.99",而你的代码直接进行了数学运算:
const priceWithTax = productData.price * 1.21; // "99.99" * 1.21 = NaN这个NaN穿过整个系统,最终存入数据库。用户看到"NaN元"的价格,完全不明白发生了什么。
TypeScript不是能防止这种错误吗?为什么检查不出这个问题?
事实可能让你惊讶:浏览器和Node.js根本看不到你写的TypeScript类型。
当TypeScript代码被编译成JavaScript时,会发生"类型擦除"——所有的interface、type、类型注解都会被删除。
你写的代码:
interface Product {
name: string;
price: number;
}
const data = req.body as Product;编译后变成:
const data = req.body; // 类型信息完全消失类型完全消失了。as Product只是告诉TypeScript编译器:"请相信这个数据符合Product类型"。但在运行时,JavaScript引擎对此一无所知。
为什么这样设计?因为TypeScript的类型系统主要目的是帮助开发者写代码时发现错误,而不是在运行时保护数据安全。这是两个不同的问题。
可以这样理解:类型系统就像代码审查时的检查清单。审查完成后,清单就完成了使命,不会跟着代码进入生产环境。
关键问题在于:你的应用接收的数据可能来自任何地方,而且这些数据不可控:
客户端JavaScript代码可能有bug
移动端App可能是旧版本
第三方api可能改变了接口
用户可能手动篡改请求
其他团队维护的库可能返回新格式
没有任何数据会在进入系统时说:"我完全符合你的Product类型,请放心使用"。JSON就是JSON,它不会主动告诉你它的类型结构。
TypeScript做不到的事情很简单:它看不到运行时的真实数据。它只能检查你写的代码逻辑是否正确。
解决办法其实很直接:在数据进入系统时,主动验证它的格式。
这就是Zod这类库存在的意义。Zod是一个数据验证库,可以在运行时检查数据是否符合预期。它的聪明之处在于能从验证规则自动推导出TypeScript类型,这样你就不需要维护两份代码了。
import { z } from 'zod';
// 一份代码,同时定义验证规则和TypeScript类型
const ProductSchema = z.object({
name: z.string(),
price: z.number().positive(),
});
// TypeScript类型自动推导
type Product = z.infer<typeof ProductSchema>;现在,Product类型和验证规则完全一致,不会出现类型说是数字、实际却收到字符串的问题。
原来的做法(盲目信任TypeScript类型)
interface CreateProductDto {
name: string;
price: number;
}
app.post('/products', (req, res) => {
// 直接相信req.body符合类型
const productData = req.body as CreateProductDto;
// price可能根本不是数字,结果是NaN
const priceWithTax = productData.price * 1.21;
db.products.create(productData);
res.send('OK');
});问题:
数据没有真正验证
错误数据进入系统
数据库被污染
用户得不到明确反馈
使用Zod的做法(验证每个数据)
import { z } from 'zod';
const CreateProductSchema = z.object({
name: z.string().min(1),
price: z.number().positive(),
});
type CreateProductDto = z.infer<typeof CreateProductSchema>;
app.post('/products', (req, res) => {
// 主动验证数据
const result = CreateProductSchema.safeParse(req.body);
// 验证失败,拒绝请求
if (!result.success) {
return res.status(400).json({
error: result.error.issues,
});
}
// 通过验证的数据是安全的
const productData = result.data;
const priceWithTax = productData.price * 1.21; // 保证是数字
db.products.create(productData);
res.send('OK');
});优势:
每个数据都经过检查
无效数据被立即拒绝
客户端收到清晰错误提示
类型和验证规则始终保持同步
流程对比:
信任方式:请求 → TypeScript说"检查过了" → 直接使用 → 可能出问题
验证方式:请求 → Zod检查数据格式 → 检查通过才能用 → 安全
→ 检查失败 → 返回错误信息 → 客户端知道问题所在
客户端调用API时,也应该验证返回的数据:
const ProductResponseSchema = z.object({
id: z.number(),
name: z.string(),
price: z.number(),
});
const fetchProduct = async (id: string) => {
const response = await fetch(`/api/products/${id}`);
const data = await response.json();
// 不要盲目信任服务器返回的数据格式
const result = ProductResponseSchema.safeParse(data);
if (!result.success) {
throw new Error('服务器返回的数据格式不对');
}
return result.data;
};这样做的好处是,即使后端改了接口,前端也能立即发现问题,而不是默默处理错误数据。
如果你使用react Hook Form,Zod可以无缝集成:
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
const LoginSchema = z.object({
email: z.string().email('请输入有效邮箱'),
password: z.string().min(6, '密码至少6位'),
});
type LoginForm = z.infer<typeof LoginSchema>;
function LoginForm() {
const { register, handleSubmit, formState: { errors } } = useForm<LoginForm>({
resolver: zodResolver(LoginSchema),
});
const onSubmit = (data: LoginForm) => {
console.log(data);
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
<input {...register('email')} placeholder="邮箱" />
{errors.email && <p>{errors.email.message}</p>}
<input {...register('password')} type="password" placeholder="密码" />
{errors.password && <p>{errors.password.message}</p>}
<button type="submit">登录</button>
</form>
);
}Zod和React Hook Form配合使用,验证规则统一,错误提示自动展示。
从数据库取出的数据也可能格式不对(比如数据迁移期间字段变化),验证一下更稳妥:
const DbUserSchema = z.object({
id: z.number(),
email: z.string(),
createdAt: z.string().datetime(),
});
const getUser = async (userId: number) => {
const row = await db.query('SELECT * FROM users WHERE id = ?', [userId]);
// 验证数据库返回的数据格式
const result = DbUserSchema.safeParse(row[0]);
if (!result.success) {
throw new Error('数据库数据格式不符');
}
return result.data;
};会不会影响性能?
验证一个对象的耗时是微秒级别的。一次网络请求或数据库查询通常需要毫秒级甚至秒级的时间。Zod的开销几乎可以忽略不计。相比之下,一个bug导致的加班、处理数据污染的成本要高得多。
定义这么多schema会不会很麻烦?
实际上更简洁。以前你需要写TypeScript接口,再写验证函数,两份代码要维护。现在只需要一个schema:
// 以前的方式(两份代码)
interface User {
id: number;
email: string;
age: number;
}
function validateUser(data: unknown): data is User {
// 手写验证逻辑...
}
// Zod的方式(一份代码)
const UserSchema = z.object({
id: z.number(),
email: z.string().email(),
age: z.number().positive(),
});
type User = z.infer<typeof UserSchema>;代码更少,而且不会出现类型和验证逻辑不同步的问题。
TypeScript的类型注解还不够吗?
类型注解对你写的代码很有帮助。但对于外部数据,TypeScript无能为力。因为外部数据在编译时不存在,TypeScript看不到它们。
简单说:TypeScript保护代码质量,Zod保护数据安全。两者都需要。
如果你还没使用Zod或类似的验证库,建议这样开始:
从API端点开始,给每个接收用户数据的端点加上验证
然后是第三方API调用,验证返回的数据格式
逐步扩展到表单、数据库查询等地方
不需要一次性改造整个项目,从最容易出问题的地方开始就行。
这不是否定TypeScript的价值。TypeScript很棒,让我们写代码时更有信心。但它不是万能的。在生产环境中,还需要真正的守卫来检查每个进入系统的数据。
使用Zod之后,深夜修复bug的情况会减少很多。而且当问题出现时,错误信息会很清晰,能快速判断是客户端还是服务端的问题。
这样的工作方式,会让你的开发体验更好,代码也更健壮。
本文内容仅供个人学习/研究/参考使用,不构成任何决策建议或专业指导。分享/转载时请标明原文来源,同时请勿将内容用于商业售卖、虚假宣传等非学习用途哦~感谢您的理解与支持!
在JavaScript中存在这样两种原始类型:Null与Undefined。这两种类型常常会使JavaScript的开发人员产生疑惑,在什么时候是Null,什么时候又是Undefined?Undefined类型只有一个值,即undefined。当声明的变量还未被初始化时,变量的默认值为undefined。
主要介绍了JS中检测数据类型的几种方式,typeof运算符用于判断对象的类型,但是对于一些创建的对象,它们都会返回\'object\',有时我们需要判断该实例是否为某个对象的实例,那么这个时候需要用到instanceof运算符
对于object和number、string、boolean之间的转换关系,ToPrimitive是指转换为js内部的原始值,如果是非原始值则转为原始值,调用valueOf()和toString()来实现。
Undefined类型表示未定义,它的类型只有一个值为undefined。undefined和null有一定的表意差别。非整数的Number类型无法使用 == 或 === 来比较,因为 JS 是弱类型语言,所以类型转换发生非常频繁
近在做项目代码重构,其中有一个要求是为代码添加智能提示和类型检查。智能提示,英文为 IntelliSense,能为开发者提供代码智能补全、悬浮提示、跳转定义等功能,帮助其正确并且快速完成编码。
基本类型:按值访问,可以操作保存在变量中实际的值;引用类型数据存在堆内存,而引用存在栈区,也就是说引用类型同时保存在栈区和堆区,关于==的执行机制,ECMASript有规范,因为==前后的值交换顺序,返回的值也是一样的,所以在此对规范做出如下总结
JavaScript 是一种弱类型或者说动态类型语言。所以你不用提前声明变量的类型,在程序运行时,类型会被自动确定,你也可以使用同一个变量保存不同类型的数据。
js的值传递和引用(地址)传递:js的5种基本数据类型 number,string,null,undefined,boolean 在赋值传递时是值传递,js的引用数据类型(object,array,function)进行引用传递,其实底层都是对象。
JS中所有的值都可以转换成布尔类型 使用Boolean()或者 !!(两个感叹号),JS中所有的值都可以转换成数字类型,使用Number()或+。数字类型转换场景目的只有一个,用于计算,将后台传递的数据,从字符串转换为数字并参与计算
众所周知,JS在很多情况下会进行强制类型转换,其中,最常见两种是:1.使用非严格相等进行比较,对==左边的值进行类型转换2.在if判断时,括号内的值进行类型转换,转化为布尔值
内容以共享、参考、研究为目的,不存在任何商业目的。其版权属原作者所有,如有侵权或违规,请与小编联系!情况属实本人将予以删除!