如何用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
相关产品推荐
相关产品推荐

