TypeScript中如何优雅处理ant-design-vue的a-select的默认空值

如果你也使用ant-design-vue和TypeScript,你可能遇到过和我一样的问题:在TypeScript环境下使用ant-design-vue的a-select组件时,如何优雅地处理默认空值。听起来好像很复杂,实际一点也不简单。

空值三兄弟:""nullundefined

当我们想让a-select默认显示placeholder时,通常会想到这三个"falsy"值:空字符串""nullundefined

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);
}

以及stringifySafely的实现

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);
}

undefinedJSON.strigify()的序列化过程中会被忽略,下面是引用的MDN的原文,可以看到更多细节:

undefined、任意的函数以及 symbol 值,在序列化过程中会被忽略(出现在非数组对象的属性值中时)或者被转换成 null(出现在数组中时)。函数、undefined 被单独转换时,会返回 undefined,如JSON.stringify(function(){}) or JSON.stringify(undefined).

当然这不仅仅是axios的问题,如果你使用fetch APIcontent-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中进行处理。
相关推荐
mfxcyh8 分钟前
antv x6使用(支持节点排序、新增节点、编辑节点、删除节点、选中节点)
vue.js·elementui·antv x6
华洛35 分钟前
聊一下MCP,希望能让各位清醒一点吧🧐
前端·javascript·vue.js
光影少年42 分钟前
vue事假机制都有哪些
前端·vue.js
孟陬1 小时前
利用 caniuse 结合 browserslist 对 JS 做兼容性检测
typescript·eslint·trae
林太白2 小时前
企业级NestJS如何创建项目学起来
前端·vue.js·后端
橙某人2 小时前
横向图片选择器之自动滚动定位功能-Javascript、Vue
前端·javascript·vue.js
.切切切 切萝卜2 小时前
【编写Node接口;接口动态获取VUE文件并异步加载, 并渲染impoort插件使用】
vue.js·前端框架·vue
天天鸭2 小时前
都2025了你不会用‘容器查询’适配屏幕?,只知道媒体查询?
前端·javascript·vue.js
Kagol3 小时前
🎉TinyPro v1.2.0 正式发布,趁着 TinyPro 项目刚创建不久,快来参与贡献(蹭 PR)吧!
前端·vue.js·nestjs
Michael.Scofield3 小时前
vue: router基础用法
前端·javascript·vue.js