You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何用io-ts定义遵循「每个平台仅选一项」规则的对象数组类型

使用io-ts实现带platform唯一性校验的数组类型

问题背景

你需要用io-ts定义一个数组类型,数组元素可选值如下:

{ platform: 'A', metric: null }
{ platform: 'B', metric: 'A' }
{ platform: 'B', metric: 'B' }
{ platform: 'B', metric: 'C' }

核心要求是数组中每个platform只能出现一次,现有代码仅定义了元素的合法类型,但无法限制platform的唯一性,比如包含两个platform: 'B'的数组会被错误地判定为有效。

现有代码:

import * as t from 'io-ts'

export const MyType = t.type({
  // ...其他内容

  metrics: t.array(t.union([
    t.type({ platform: t.literal('A'), metric: t.nullType }),
    t.type({ platform: t.literal('B'), metric: t.literal('A') }),
    t.type({ platform: t.literal('B'), metric: t.literal('B') }),
    t.type({ platform: t.literal('B'), metric: t.literal('C') })
  ]))
})

export type MyType = t.TypeOf<typeof MyType>

有效/无效示例:

const var1: MyType = [{ platform: 'A', metric: null }] // 有效
const var2: MyType = [{ platform: 'B', metric: 'A' }] // 有效
const var3: MyType = [{ platform: 'A', metric: null }, { platform: 'B', metric: 'A' }] // 有效
const var4: MyType = [{ platform: 'B', metric: 'A' }, { platform: 'B', metric: 'B' }] // 无效(但现有代码无法识别)

解决方案:自定义io-ts类型实现唯一性校验

io-ts的内置t.array不支持这种自定义的唯一性校验,需要通过自定义Codec来扩展功能,步骤如下:

1. 抽离单个元素的类型定义

先把数组元素的合法类型单独抽出来,方便后续复用:

import * as t from 'io-ts'
import { failure } from 'io-ts/lib/PathReporter'

// 定义单个metrics元素的合法类型
const MetricItem = t.union([
  t.type({ platform: t.literal('A'), metric: t.nullType }),
  t.type({ platform: t.literal('B'), metric: t.union([t.literal('A'), t.literal('B'), t.literal('C')]) })
])
export type MetricItem = t.TypeOf<typeof MetricItem>

2. 创建带唯一性校验的数组类型

自定义一个继承自t.Array<MetricItem>的类型,重写decode方法来添加platform唯一性检查:

// 自定义数组类型:保证每个platform只出现一次
const UniquePlatformMetrics = new t.Type<MetricItem[], MetricItem[], unknown>(
  'UniquePlatformMetrics',
  // 类型守卫:先检查是否是MetricItem数组,再检查platform唯一性
  (u): u is MetricItem[] => {
    if (!t.array(MetricItem).is(u)) return false
    const platforms = u.map(item => item.platform)
    // 用Set去重后长度和原数组一致则说明无重复
    return new Set(platforms).size === platforms.length
  },
  // decode方法:先解码为MetricItem数组,再检查唯一性
  (u, c) => {
    const decoded = t.array(MetricItem).decode(u)
    if (decoded._tag === 'Left') {
      return decoded
    }
    const platforms = decoded.right.map(item => item.platform)
    const uniquePlatforms = new Set(platforms)
    if (uniquePlatforms.size !== platforms.length) {
      // 找出重复的platform
      const duplicates = platforms.filter(p => platforms.indexOf(p) !== platforms.lastIndexOf(p))
      return failure(c, `数组中存在重复的platform: ${[...new Set(duplicates)].join(', ')}`)
    }
    return decoded
  },
  // encode方法直接复用数组的encode
  t.array(MetricItem).encode
)

3. 整合到你的MyType中

现在把自定义的数组类型替换原来的t.array:

export const MyType = t.type({
  // ...其他内容
  metrics: UniquePlatformMetrics
})

export type MyType = t.TypeOf<typeof MyType>

4. 测试验证

现在再测试之前的示例,var4会被正确判定为无效:

// 有效示例
const var1: MyType = { metrics: [{ platform: 'A', metric: null }] }
const var2: MyType = { metrics: [{ platform: 'B', metric: 'A' }] }
const var3: MyType = { metrics: [{ platform: 'A', metric: null }, { platform: 'B', metric: 'A' }] }

// 无效示例:会抛出错误,提示重复的platform: B
const var4 = MyType.decode({ metrics: [{ platform: 'B', metric: 'A' }, { platform: 'B', metric: 'B' }] })
console.log(failure(var4)) // 输出错误信息

备选思路:改用对象结构(更简洁)

如果业务场景允许,也可以把metrics从数组改成对象,用platform作为key,这样天然保证每个platform唯一:

const MetricsObject = t.partial({
  A: t.type({ platform: t.literal('A'), metric: t.nullType }),
  B: t.union([
    t.type({ platform: t.literal('B'), metric: t.literal('A') }),
    t.type({ platform: t.literal('B'), metric: t.literal('B') }),
    t.type({ platform: t.literal('B'), metric: t.literal('C') })
  ])
})

export const MyType = t.type({
  // ...其他内容
  metrics: MetricsObject
})

// 有效示例
const validObj: MyType = {
  metrics: {
    A: { platform: 'A', metric: null },
    B: { platform: 'B', metric: 'A' }
  }
}

这种方式不需要自定义校验,io-ts会自动保证每个platform最多出现一次,但结构从数组变成了对象,需要根据实际业务需求选择。

内容的提问来源于stack exchange,提问作者LakeToya

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.05 19:15:31