如果你也使用ant-design-vue和TypeScript,你可能遇到过和我一样的问题:在TypeScript环境下使用ant-design-vue的a-select组件时,如何优雅地处理默认空值。听起来好像很复杂,实际一点也不简单。
空值三兄弟:""
、null
、undefined
当我们想让a-select默认显示placeholder时,通常会想到这三个"falsy"值:空字符串""
、null
和undefined
vue
<template>
<a-select v-model:value="fruit" placeholder="请选择你的水果" />
</template>
<script lang="ts" setup>
import { ref } from 'vue'
const fruit = ref() // ? ref('')? ref(null)? ref(undefined)?
</script>
那就逐一尝试一下。
空字符串""
当你赋予空字符串,会发现本该在空值时显示的placeholder并没有显示,这是因为ant-design-vue认为空字符串是一个有意义的值,而非空值(这一点后文的issue截图中可以印证)。
null
那null
怎么样呢,尝试发现placeholder显示正常,但编辑器会触发TypeScript报错提示:不能将类型"null"分配给类型"SelectValue"。
(当然,如果你不使用TypeScript,就没有这个问题,直接使用null没有任何问题)。
html
<template>
<a-select v-model:value="fruit" />
<!-- ^? 不能将类型"null"分配给类型"SelectValue"。 -->
</template>
我们来看下SelectValue
的类型定义:
ts
export declare type SelectValue = RawValue | RawValue[] | LabeledValue | LabeledValue[] | undefined;
ts
declare type RawValue = string | number;
export interface LabeledValue {
key?: string;
value: RawValue;
label?: any;
}
undefined
从类型定义可以看到,官方应该是希望我们使用undefined
作为默认空值。使用undefined
也确实不影响placeholder的显示,但会有另一个更致命的问题:值为undefined的字段在fetch/XHR请求中很可能会被过滤掉。
我使用axios作为请求库,当我使用content-type: application/json
发送带有值为undefined
的字段时,该字段会被过滤掉而不会出现在最终的Request Payload中。这是因为axios在源码中会使用JSON.stringify()
来序列化请求参数。
这部分源码在lib/defaults/index.js
js
if (isObjectPayload || hasJSONContentType ) {
headers.setContentType('application/json', false);
return stringifySafely(data);
}
js
function stringifySafely(rawValue, parser, encoder) {
if (utils.isString(rawValue)) {
try {
(parser || JSON.parse)(rawValue);
return utils.trim(rawValue);
} catch (e) {
if (e.name !== 'SyntaxError') {
throw e;
}
}
}
return (encoder || JSON.stringify)(rawValue);
}
undefined
在JSON.strigify()
的序列化过程中会被忽略,下面是引用的MDN的原文,可以看到更多细节:
undefined
、任意的函数以及 symbol 值,在序列化过程中会被忽略(出现在非数组对象的属性值中时)或者被转换成null
(出现在数组中时)。函数、undefined 被单独转换时,会返回 undefined,如JSON.stringify(function(){})
orJSON.stringify(undefined)
.
当然这不仅仅是axios的问题,如果你使用fetch API
,content-type: application/json
的请求可能也需要使用JSON.stringfy
来处理,MDN的示例代码里就是这么做的。其它的请求库我没有求证,大概率也有这个问题。
社区的声音
null
运行时表现正常,但类型不匹配。undefined
类型匹配,但运行时不特殊处理会出bug的啊。ant-design-vue的官方为什么就不能把null
纳入SelectValue
的类型联合中呢,这好像是最好的解决方案。其实不光是我这么想,早在2019年社区就有人提了这个issue
也有人建议官方把null
纳入空值,但官方并没有做出回应,该issue已被超时自动关闭。并且截至目前为止的最新版本4.2.6
该问题依旧存在。

解决方案
那能怎么办呢,官方不解决,我的业务代码还得写啊。我想到的解决方案有两种:
方案1
第一种方案是修改node_modules
包中SelectValue
的类型定义,并使用patch-package来打补丁。但为这么小的一类型问题打一个补丁,感觉是杀鸡用牛刀了。
方案2
第二种方案是在发送请求的时候手动把所有值为undefined
的字段替换为null
。既然是全局的,肯定不能每个请求的地方都执行一遍这个逻辑,axios的请求配置transformRequest
允许在向服务器发送前,修改请求数据,这是最合适的修改方式。
我们先来实现这个数据转换的核心函数,需要考虑到对象深层的递归处理:
ts
/**
* 递归替换对象中所有的 undefined 为 null
* @param obj
* @returns
*/
function replaceUndefinedWithNull(obj: Record<string, any>): Record<string, any> {
if (typeof obj !== 'object' || obj === null) {
return obj // 如果不是对象或数组,直接返回
}
// 处理数组
if (Array.isArray(obj)) {
return obj.map((item) => replaceUndefinedWithNull(item))
}
// 处理普通对象
const result: Record<string, any> = {}
for (const key in obj) {
if (obj.hasOwnProperty(key)) {
const value = obj[key]
if (value === undefined) {
result[key] = null // 替换 undefined 为 null
} else {
result[key] = replaceUndefinedWithNull(value) // 递归处理嵌套对象或数组
}
}
}
return result
}
然后是配置,需要注意,我们直接给transformRequest
赋新值会覆盖掉axios内置的transformRequest
,我们只需要把内置的默认transformRequest
取出来再塞进去即可:
ts
axios.create({
transformRequest: [
replaceUndefinedWithNull,
(axios.defaults.transformRequest as AxiosTransformer[])[0],
],
// other configs
})
总结
本来是个小问题,但还是展开说了不少。就连五星评论家麦克阿瑟也忍不住总结道:

- 在TypeScript中使用ant-design-vue的a-select组件时,如何处理默认空值是个问题:空字符串不能正常显示placeholder;
null
会导致TypeScript类型报错;undefined
很可能会导致在请求发起时被从最终的请求参数中过滤掉该字段。这是因为JSON.stringify()
序列化会忽略掉undefined
。 - 如果不使用TypeScript,直接使用
null
是最简单的方案。 - 社区有声音呼吁官方把
null
纳入空值处理,但并没有得到回应和处理。 - 比较好的解决方案是在发起请求前统一处理请求参数,比如axios可以在
transformRequest
中进行处理。