Vue 3 严格模式下,可选接口字段为什么会炸:一次上架页 TypeScript 复盘

7 阅读3分钟

前端经常会遇到展示文案涉及字符串拼接,例如「更新频率」「数据规模」等字段。JavaScript 里 a + b 很常见;打开 TypeScript 严格检查后,同一行会报「可能为未定义」。根因不是业务写错,而是接口类型标了可选,运行时拼接却当必填字符串用

背景

catalogData 的类型是 API.IPutawayProductDetailRes。其中这些字段都带 ?

dataSize?: string;
dataSizeUnit?: string;
updateFrequency?: string;
updateFrequencyUnit?: string;
personalInformation?: number;
authorize?: string;

? 的意思是:值类型是 T | undefined。解构后,变量在类型上就是可选的,不能当「一定存在的 string」做运算。


问题 1:可选字符串直接 + 拼接

报错

“updateFrequencyUnit”可能为“未定义”。
“dataSize”可能为“未定义”。

出错写法

updateFrequencyStr: updateFrequency + updateFrequencyUnit
dataSizeStr: dataSize + dataSizeUnit

+ 在两端都是 string 时才是拼接。string | undefined 不能直接参与,因为 undefined + '次' 在类型上不合法(运行时还会拼出 "undefined次")。

修法

用空串兜底,再模板拼接:

updateFrequencyStr: `${updateFrequency ?? ''}${updateFrequencyUnit ?? ''}`
dataSizeStr: `${dataSize ?? ''}${dataSizeUnit ?? ''}`

?? 只在 null / undefined 时替换,不会把合法的 0 吃掉。展示层已有 || '-',两边都空时仍显示 -

不要用 ! 断言(updateFrequencyUnit!)来消错:类型过了,接口没回这个字段时仍可能拼出错误文案。


问题 2:函数参数类型比真实字段窄

报错

类型“string | undefined”的参数不能赋给类型“number | undefined”的参数。

出错写法

function getIsCover(val?: number): string {
  return val === 1 ? '是' : '否';
}

personalInformation: getIsCover(personalInformation) // number | undefined,OK
authorize: getIsCover(authorize)                      // string | undefined,不匹配

接口里「是否」并不统一:personalInformationnumberauthorize'0' | '1' 这种字符串。函数只收 number,调用就被拦下。

修法

把入参放宽,并把 '1'1 都当成「是」:

function getIsCover(val?: number | string): string {
  return val === 1 || val === '1' ? '是' : '否';
}

这是对齐真实契约,不是关掉检查。后端 0/1 和 '0'/'1' 混用时,只认一种会在运行时显示错。


问题 3:解构了一个类型上不存在的字段

报错

类型“IPutawayProductDetailRes”上不存在属性“resourceNames”。

组件按 resourceNames 拼「关联数据资源」,但类型里对应的是 resourceId?: string[]。从 props.catalogData 解构 resourceNames,在严格检查下就是「对象上没有这个键」。

修法

先确认键是否存在、值是否为数组,再 join

resourceNamesStr:
  'resourceNames' in props.catalogData && Array.isArray(props.catalogData.resourceNames)
    ? props.catalogData.resourceNames.join(',')
    : '-'

'x' in obj 会把后续访问收窄。长期更干净的做法是:类型补上 resourceNames,或展示改用已声明的 resourceId


问题 4:字典 Ref 可能为 undefined

报错

“deliveryTypeOptions.value”可能为“未定义”。

useDict 返回的是 Ref<T[] | undefined>,首屏可能还没拉到字典。.value.find(...) 默认假定 .value 一定是数组。

修法

可选链:

deliveryTypeOptions.value?.find((item) => item.value === formData.value.deliveryType)

没有字典时 findundefined,后面的 if (find) 本来就能兜住。


问题 5:修完逻辑后多出来的 unused

去掉拼接后,updateFrequencyUnitList 字典不再使用,eslint 报 assigned but never used。删掉未用的 useDict 即可,避免「为了展示单位去拉字典、实际却拼原始码」。


对照表

现象类型原因做法
A + B 报未定义T | undefined 不能直接 +`${a ?? ''}${b ?? ''}`
getIsCover 参数不匹配接口 0/1 与 '0'/'1' 混用参数写成 number | string,两种都判断
没有 resourceNames展示字段和类型声明不一致in + Array.isArray,或改类型/改字段
.value.find 报未定义Ref 初始可能是 undefined?.find
unused 字典重构后引用消失删掉

可复用的原则

  1. 可选字段先当「可能没有」,再决定展示;不要靠 JS 的 undefined + ''
  2. 函数入参跟接口走,不要想当然全是 number
  3. 解构前确认键在类型里;运行时多出来的字段要用收窄,不要假装类型有它。
  4. 异步字典、表单实例一律 ?.
  5. 不要用 as / ! 糊过去;这次是展示层兜底,所以用 ?? ''|| '-' 就够。

一句话:严格 TypeScript 逼你把「后端可能不给、字典可能没到、0 和 '1' 可能混用」写进代码。修的不是报错文案,是把展示逻辑和接口契约对齐。