首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >类型安全 ≠ 数据安全:TypeScript 没承诺过的事

类型安全 ≠ 数据安全:TypeScript 没承诺过的事

作者头像
前端达人
发布2026-07-22 19:20:07
发布2026-07-22 19:20:07
290
举报
文章被收录于专栏:前端达人前端达人
image
image

TypeScript 今年在 GitHub 上把 Python 和 JavaScript 都挤下去了,成了用的人最多的语言。我天天写它,编辑器一片绿,编译一次过。

然后呢?我照样把一个带 bug 的表单发上了线,三周后才被工单砸醒。

今天就聊聊这事:TypeScript 给你的那种"稳了"的感觉,到底能信几分。

先说说这个 bug 是怎么来的

场景很日常。我们做的是一个管理后台,里面有很多下拉框——商品产地、所属分类、审批状态,这种。

这些下拉框的选项不是写死在前端代码里的,而是运营同学在后台自己配的。运营在配置页里一行写一个选项,存到数据库里就是一个用换行符隔开的字符串,比如:

代码语言:javascript
复制
小麦
水稻
玉米

前端拿到这个字符串,切一切,转成 Ant Design 下拉框要的格式。我写的转换函数长这样:

代码语言:javascript
复制
interface DocFieldOption {
  label: string;   // 显示的文字
  value: string;   // 选中后的值
}

function mapOptions(optionsString: string): DocFieldOption[] {
  return optionsString
    .split('\n')                              // 按换行切开
    .map(opt => ({ label: opt, value: opt })); // 转成下拉框要的格式
}

类型写得清清楚楚,测了三个表单,全都好使。上线。

三周后来了张工单:某个下拉框里,多了一个空白的、点不动的选项。 就那么一条空行,杵在"水稻"和"玉米"中间,特别诡异。

凶手是一个回车

排查下来,原因简单到想笑:运营同学配选项的时候手抖多敲了个回车,存进数据库的字符串变成了:

代码语言:javascript
复制
"小麦\n水稻\n\n玉米"
       注意这里 ↑ 连着两个换行

split('\n') 一切,中间就多出来一个空字符串 ''。我的代码毫无怨言地把它包装成了 { label: '', value: '' }——于是下拉框里就多了那行空白。

这时候你可能想问:TypeScript 呢?它不是号称类型安全吗?怎么一声不吭?

因为它没错。空字符串也是字符串,{ label: '', value: '' } 完全符合我定义的接口。TypeScript 检查的是"你的代码有没有按你说的形状来处理数据"——我的代码确实按说好的形状处理了,它当然放行。

问题是,数据本身不按说好的来,这事 TypeScript 根本看不见。

打个比方你就懂了

TypeScript 就像收货处贴在墙上的一张验收标准:"本仓库只收贴了面单的箱子,面单上必须写品名和数量。"

这张标准管得住你自己仓库里的流程。但供应商那头发什么货,它管不着——供应商压根没看过你这张标准。人家发来一个贴着面单、但里面是空的箱子,完全不违反"必须贴面单"这条规矩。

放到咱们的场景里:

  • 你的 TypeScript 接口 = 贴在墙上的验收标准
  • 后端返回的数据 = 供应商发来的货
  • 运营在后台改配置 = 供应商换了包装方式,但没通知你

画成图就是这样,注意中间那条线:

代码语言:javascript
复制
      后端这边(没有编译检查)    │    前端这边(有 TypeScript)
  ──────────────────────────────┼──────────────────────────────
                                │
   运营在后台改下拉选项           │    interface 写得再好
          ↓                     │         ↓
   数据库里的配置变了             │    tsc 编译通过 ✅
          ↓                     │         ↓
   接口原样把数据吐给前端 ───────→│──→  组件拿到就渲染
                                │
                         TypeScript 的地盘
                         只到这条线右边

线的左边发生任何事——选项改了、必填变可选了、多了个空行——你的编译不会报错,CI 不会变红,一切静悄悄。等你知道的时候,一般已经是工单了。

修起来其实特别简单

回到那个函数,修复版就加了两行:

代码语言:javascript
复制
// TypeScript 版
function mapOptions(optionsString: string): DocFieldOption[] {
  return optionsString
    .split('\n')
    .map(opt => opt.trim())      // 把首尾空格顺手去掉
    .filter(Boolean)             // 空字符串直接扔掉,就这行救命
    .map(opt => ({ label: opt, value: opt }));
}
代码语言:javascript
复制
// JavaScript 版,一模一样的逻辑
function mapOptions(optionsString) {
  return optionsString
    .split('\n')
    .map(opt => opt.trim())
    .filter(Boolean)
    .map(opt => ({ label: opt, value: opt }));
}

filter(Boolean) 的意思是:空字符串在 JS 里算"假值",直接过滤掉。相当于收货的时候顺手把空箱子扔了,别让它上货架。

一行代码的事。要是第一天就有它,这 bug 活不过五分钟。

顺便说一句,这篇文章里的代码我都实际跑过:用 tsc --strict 编译零报错,拿 "小麦\n水稻\n\n玉米\n \n高粱 " 这种带空行带空格的脏数据实测,修复版输出干干净净的 4 个选项,原版确实会吐出空选项。放心抄。

更值钱的场景:开箱验货

下拉框多个空行还算小事,用户顶多觉得怪。但如果是订单金额、库存数量、审批状态这种直接影响业务的数据呢?后端哪天悄悄把数字改成字符串、把字段改成可能为 null,你的页面轻则显示 NaN,重则算错钱。

这种地方,光扔空箱子不够,得开箱验货——数据进来先检查一遍,形状不对当场报警,而不是带病渲染。最朴素的写法:

代码语言:javascript
复制
// TypeScript 版:一个"验货员"函数
function isValidOptions(data: unknown): data is DocFieldOption[] {
  return Array.isArray(data) &&
    data.every(item =>
      typeof item === 'object' && item !== null &&
      typeof (item as any).label === 'string' &&
      (item as any).label.length > 0 &&        // label 不许是空的
      typeof (item as any).value === 'string'
    );
}
代码语言:javascript
复制
// JavaScript 版
function isValidOptions(data) {
  return Array.isArray(data) &&
    data.every(item =>
      typeof item === 'object' && item !== null &&
      typeof item.label === 'string' &&
      item.label.length > 0 &&
      typeof item.value === 'string'
    );
}

我拿六种脏数据测过这个函数:空 label、缺字段、value 是数字、数组里混 null、整个不是数组……全部正确拦截,合法数据正常放行。

要是项目里这种边界很多,手写验货员会烦死,可以直接上 zod 这个库,写一份 schema,类型和校验都有了:

代码语言:javascript
复制
import { z } from 'zod';

const OptionSchema = z.object({
  label: z.string().min(1),   // min(1):空字符串直接进不来
  value: z.string(),
});
const OptionsSchema = z.array(OptionSchema);

// 类型直接从 schema 推出来,不用再手写 interface
type DocFieldOption = z.infer<typeof OptionSchema>;

// 接口数据进来先过这一道,不合法当场抛错
const options = OptionsSchema.parse(await fetchOptions());

这些代码平时看着都像多余的,直到某天运营改了配置又没人告诉你——那一刻它就是"控制台一个明确的报错"和"生产环境一行诡异空白"的区别。

给你留个小作业

这周抽十分钟,在你自己的项目里 全局搜一下 Promise<any> ,尤其是封装请求的那几个文件。数数有多少个接口返回值,没做任何检查就直接进了页面渲染。

我出完这个 bug 之后搜过一次。那个数字让我沉默了一会儿。

最后总结成一句话,也是我这几年最大的心得:

TypeScript 保证的是"你按说好的形状写了代码",从来没保证过"世界会按说好的形状给你数据"。 前者归编译器管,后者归你自己管。尤其在运营、产品随手就能在后台改配置的系统里,这不是什么小概率事件——

它就是普普通通、随时会发生的星期二。


评论区聊聊:我这次是被一个空行坑了,但被"后端数据跟说好的不一样"坑过的,肯定不止我一个——字段突然变 null、数字变字符串、时间戳从秒变毫秒……你踩过最离谱的数据坑是什么?

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2026-07-21,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 前端达人 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 先说说这个 bug 是怎么来的
  • 凶手是一个回车
  • 打个比方你就懂了
  • 修起来其实特别简单
  • 更值钱的场景:开箱验货
  • 给你留个小作业
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档