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

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

深夜加班排查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如何将String转化为Int

字符串到整型的一个转换,在面试过程中或者在工作中都会频繁遇到,那么string到js怎么转换呢 ?parseInt方法在format\'00\'开头的数字时会当作2进制转10进制的方法进行转换

Typescript内置类型与自定义类型

大家用过 Typescript 都清楚,很多时候我们需要提前声明一个类型,再将类型赋予变量。例如在业务中,我们需要渲染一个表格,往往需要定义:

Js实现base64,file和blob相互转换

JavaScript实现base64,file和blob相互转换:base64转为Blob;Blob转为base64;base64转换为file;js图片转换为base64;在Java中base64和File相互转换

Js的6种基本数据类型

在JS中一共有六种数据类型 String:字符串 Number:数值 Boolean:布尔值 Null:空值 Undefined:未定义 Object:对象 ,其中String,Number,Boolean,Null,Undefined属于基本数据类型而Object属于引用数据类型

JavaScript判断字符串是否为数字类型

JavaScript中有Number.isInteger可以判断一个字符串是否为整数。不过目前JS没有内置的函数来判断一个数字是否为包含小数的数字:

TypeScript never 类型

在类型理论(数学逻辑中的一种理论)中, 底部类型 是没有值的类型。也称为零或空类型,有时用 falsum(⊥)表示。 数学理论与计算机的发展是相辅相成的,底部类型在计算机科学中也有一定的应用场景。

JS数据类型转换表

下表显示了将不同的JavaScript值转换为Number,String和Boolean的结果:注意:引号(\\\"\\\")中的值表示字符串值。在红色的值是程序员可能不希望被转换为的值。

Js的typeof返回哪些数据类型?

JavaScript中的数据类型:值类型(基本类型):字符串(String)、数字(Number)、布尔(Boolean)、对空(Null)、未定义(Undefined)、Symbol。引用数据类型:对象(Object)、数组(Array)、函数(Function)。

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

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

JS中的布尔 数字 字符串

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

点击更多...

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