前端经常会遇到展示文案涉及字符串拼接,例如「更新频率」「数据规模」等字段。JavaScript 里 a + b 很常见;打开 TypeScript 严格检查后,同一行会报「可能为未定义」。根因不是业务写错,而是接口类型标了可选,运行时拼接却当必填字符串用。
背景
catalogData 的类型是 API.IPutawayProductDetailRes。其中这些字段都带 ?:
ts
dataSize?: string;
dataSizeUnit?: string;
updateFrequency?: string;
updateFrequencyUnit?: string;
personalInformation?: number;
authorize?: string;
? 的意思是:值类型是 T | undefined。解构后,变量在类型上就是可选的,不能当「一定存在的 string」做运算。
问题 1:可选字符串直接 + 拼接
报错
"updateFrequencyUnit"可能为"未定义"。
"dataSize"可能为"未定义"。
出错写法
ts
updateFrequencyStr: updateFrequency + updateFrequencyUnit
dataSizeStr: dataSize + dataSizeUnit
+ 在两端都是 string 时才是拼接。string | undefined 不能直接参与,因为 undefined + '次' 在类型上不合法(运行时还会拼出 "undefined次")。
修法
用空串兜底,再模板拼接:
ts
updateFrequencyStr: `${updateFrequency ?? ''}${updateFrequencyUnit ?? ''}`
dataSizeStr: `${dataSize ?? ''}${dataSizeUnit ?? ''}`
?? 只在 null / undefined 时替换,不会把合法的 0 吃掉。展示层已有 || '-',两边都空时仍显示 -。
不要用 ! 断言(updateFrequencyUnit!)来消错:类型过了,接口没回这个字段时仍可能拼出错误文案。
问题 2:函数参数类型比真实字段窄
报错
类型"string | undefined"的参数不能赋给类型"number | undefined"的参数。
出错写法
ts
function getIsCover(val?: number): string {
return val === 1 ? '是' : '否';
}
personalInformation: getIsCover(personalInformation) // number | undefined,OK
authorize: getIsCover(authorize) // string | undefined,不匹配
接口里「是否」并不统一:personalInformation 是 number,authorize 是 '0' | '1' 这种字符串。函数只收 number,调用就被拦下。
修法
把入参放宽,并把 '1' 和 1 都当成「是」:
ts
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:
ts
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 一定是数组。
修法
可选链:
ts
deliveryTypeOptions.value?.find((item) => item.value === formData.value.deliveryType)
没有字典时 find 为 undefined,后面的 if (find) 本来就能兜住。
问题 5:修完逻辑后多出来的 unused
去掉拼接后,updateFrequencyUnitList 字典不再使用,eslint 报 assigned but never used。删掉未用的 useDict 即可,避免「为了展示单位去拉字典、实际却拼原始码」。
对照表
| 现象 | 类型原因 | 做法 |
|---|---|---|
| A + B 报未定义 | `T | undefined不能直接+` |
| getIsCover 参数不匹配 | 接口 0/1 与 '0'/'1' 混用 |
参数写成 `number |
| 没有 resourceNames | 展示字段和类型声明不一致 | in + Array.isArray,或改类型/改字段 |
.value.find 报未定义 |
Ref 初始可能是 undefined | ?.find |
| unused 字典 | 重构后引用消失 | 删掉 |
可复用的原则
- 可选字段先当「可能没有」 ,再决定展示;不要靠 JS 的
undefined + ''。 - 函数入参跟接口走 ,不要想当然全是
number。 - 解构前确认键在类型里;运行时多出来的字段要用收窄,不要假装类型有它。
- 异步字典、表单实例一律
?.。 - 不要用
as/!糊过去 ;这次是展示层兜底,所以用?? ''和|| '-'就够。
一句话:严格 TypeScript 逼你把「后端可能不给、字典可能没到、0 和 '1' 可能混用」写进代码。修的不是报错文案,是把展示逻辑和接口契约对齐。