TypeScript类型检查的真相:为什么你的代码在运行时还会出错

更新日期: 2025-11-02 阅读: 62 标签: 类型

深夜加班排查bug,发现一个奇怪的问题:TypeScript明明显示类型正确,但用户看到的价格却显示成NaN。查看日志才发现,有个客户端发送的商品价格是字符串"99.99",而你的代码直接进行了数学运算:

const priceWithTax = productData.price * 1.21; // "99.99" * 1.21 = NaN

这个NaN穿过整个系统,最终存入数据库。用户看到"NaN元"的价格,完全不明白发生了什么。

TypeScript不是能防止这种错误吗?为什么检查不出这个问题?


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检查数据格式 → 检查通过才能用 → 安全
→ 检查失败 → 返回错误信息 → 客户端知道问题所在


实际应用场景

场景1:处理API返回的数据

客户端调用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;
};

这样做的好处是,即使后端改了接口,前端也能立即发现问题,而不是默默处理错误数据。

场景2:表单验证

如果你使用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配合使用,验证规则统一,错误提示自动展示。

场景3:数据库查询结果验证

从数据库取出的数据也可能格式不对(比如数据迁移期间字段变化),验证一下更稳妥:

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或类似的验证库,建议这样开始:

  1. 从API端点开始,给每个接收用户数据的端点加上验证

  2. 然后是第三方API调用,验证返回的数据格式

  3. 逐步扩展到表单、数据库查询等地方

不需要一次性改造整个项目,从最容易出问题的地方开始就行。

这不是否定TypeScript的价值。TypeScript很棒,让我们写代码时更有信心。但它不是万能的。在生产环境中,还需要真正的守卫来检查每个进入系统的数据。

使用Zod之后,深夜修复bug的情况会减少很多。而且当问题出现时,错误信息会很清晰,能快速判断是客户端还是服务端的问题。

这样的工作方式,会让你的开发体验更好,代码也更健壮。

本文内容仅供个人学习/研究/参考使用,不构成任何决策建议或专业指导。分享/转载时请标明原文来源,同时请勿将内容用于商业售卖、虚假宣传等非学习用途哦~感谢您的理解与支持!

链接: https://fly63.com/article/detial/13119

JS中Null与Undefined的区别

在JavaScript中存在这样两种原始类型:Null与Undefined。这两种类型常常会使JavaScript的开发人员产生疑惑,在什么时候是Null,什么时候又是Undefined?Undefined类型只有一个值,即undefined。当声明的变量还未被初始化时,变量的默认值为undefined。

Javascript的类型检测

主要介绍了JS中检测数据类型的几种方式,typeof运算符用于判断对象的类型,但是对于一些创建的对象,它们都会返回\'object\',有时我们需要判断该实例是否为某个对象的实例,那么这个时候需要用到instanceof运算符

js类型转换的各种玩法

对于object和number、string、boolean之间的转换关系,ToPrimitive是指转换为js内部的原始值,如果是非原始值则转为原始值,调用valueOf()和toString()来实现。

JavaScript类型:关于类型,有哪些你不知道的细节?

Undefined类型表示未定义,它的类型只有一个值为undefined。undefined和null有一定的表意差别。非整数的Number类型无法使用 == 或 === 来比较,因为 JS 是弱类型语言,所以类型转换发生非常频繁

为你的 JavaScript 项目添加智能提示和类型检查

近在做项目代码重构,其中有一个要求是为代码添加智能提示和类型检查。智能提示,英文为 IntelliSense,能为开发者提供代码智能补全、悬浮提示、跳转定义等功能,帮助其正确并且快速完成编码。

js的类型

基本类型:按值访问,可以操作保存在变量中实际的值;引用类型数据存在堆内存,而引用存在栈区,也就是说引用类型同时保存在栈区和堆区,关于==的执行机制,ECMASript有规范,因为==前后的值交换顺序,返回的值也是一样的,所以在此对规范做出如下总结

再也不用担心 JavaScript 的数据类型转换了

JavaScript 是一种弱类型或者说动态类型语言。所以你不用提前声明变量的类型,在程序运行时,类型会被自动确定,你也可以使用同一个变量保存不同类型的数据。

JavaScript基础之值传递和引用传递

js的值传递和引用(地址)传递:js的5种基本数据类型 number,string,null,undefined,boolean 在赋值传递时是值传递,js的引用数据类型(object,array,function)进行引用传递,其实底层都是对象。

JS中的布尔 数字 字符串

JS中所有的值都可以转换成布尔类型 使用Boolean()或者 !!(两个感叹号),JS中所有的值都可以转换成数字类型,使用Number()或+。数字类型转换场景目的只有一个,用于计算,将后台传递的数据,从字符串转换为数字并参与计算

if条件中,js的强制类型转换

众所周知,JS在很多情况下会进行强制类型转换,其中,最常见两种是:1.使用非严格相等进行比较,对==左边的值进行类型转换2.在if判断时,括号内的值进行类型转换,转化为布尔值

点击更多...

内容以共享、参考、研究为目的,不存在任何商业目的。其版权属原作者所有,如有侵权或违规,请与小编联系!情况属实本人将予以删除!