TypeScript开发小状况记录之选且只选一个

作者:函数茶馆

时间:2026-10-25

浏览:245

本文记录在TypeScript开发中,如何定义一个必须包含指定字段中且仅有一个字段的对象类型。通过对比泛型显式声明与PickOne工具类型的实现,解决了联合类型无法限制多字段共存的问题,确保业务代码中字段选择的唯一性与合法性。

文章目录

前言

在项目的TypeScript开发过程中,遇到了一些报错或棘手的情况,我决定开启一个系列来记录这些问题及我的解决方法。

背景

在开发场景中,经常需要定义一个对象类型,该类型必须包含指定n个字段中的某一个。

例如,定义一个工程师(Engineer)对象,包含姓名(name)、性别(gender)、年龄(age)以及对一门编程语言(java/cpp/go/js四选一)的评价。

前三个字段较为简单,但第四个字段较为复杂:其key可能不同(value也可能不同),且必须从给定的四个选项中仅选一个。

初步方案

起初考虑使用可选或联合类型,但发现无法限制“四选一”,对无编程语言字段或多个字段并存的情况缺乏有效限制,最终采用泛型并在使用时显式声明。

于是,类型定义如下:

interface ICodingLangRating {
    java: string
    cpp: string
    go: string
    js: string
}

type Engineer<K extends keyof ICodingLangRating> = {
    name: string
    gender: 'male' | 'female'
    age: number
} & Pick<ICodingLangRating, K>

对该声明的校验代码如下:

// 正确
const candidate: Engineer<'java'> = {
    name: 'Jack',
    gender: 'male',
    age: 22,
    java: 'fabulous'
}

// 错误,声明了java,但是却同时定义了java和go字段
const candidate_1: Engineer<'java'> = {
    name: 'Jack',
    gender: 'male',
    age: 22,
    java: 'fabulous',
    go: 'not bad'
}

// 错误,声明了java,但是类型不正确
const candidate_2: Engineer<'java'> = {
    name: 'Jack',
    gender: 'male',
    age: 22,
    java: 666
}

// 错误,声明了java,但是却定义了go字段
const candidate_3: Engineer<'java'> = {
    name: 'Jack',
    gender: 'male',
    age: 22,
    go: 'not bad'
}

// 错误,声明了java,但是却同时定义了cpp和go字段
const candidate_4: Engineer<'java'> = {
    name: 'Jack',
    gender: 'male',
    age: 22,
    cpp: 'unknown',
    go: 'not bad'
}

// 错误,声明了java,但是却没有定义java字段
const candidate_5: Engineer<'java'> = {
    name: 'Jack',
    gender: 'male',
    age: 22
}

// 错误,声明了ICodingLangRating中不存在的python
const candidate_6: Engineer<'python'> = {
    name: 'Jack',
    gender: 'male',
    age: 22,
    python: 'just so so'
}

从校验代码可以看出,针对各种不符合期望的情况:

  • 声明a,却定义a和b
  • 声明a,但定义a的类型不正确
  • 声明a,却定义b
  • 声明a,却定义b和c
  • 声明a,却没有定义a
  • 声明不合法的f

都能做出正确的限制,确保在业务场景的代码中,有且只有一个合法范围的字段。

但是,转折来了!

后续使用中发现,该方案仅为弱限制,若在泛型显式声明中传入联合类型,仍可绕过“有且只有一个编程语言字段”的限制。

前言

// 正确,声明了java和go,并同时定义了java和go字段
const candidate_1: Engineer<'java' | 'go'> = {
    name: 'Jack',
    gender: 'male',
    age: 22,
    java: 'fabulous',
    go: 'not bad'
}

难道就真的没有办法做到只能选择一个的限制么?

终极方案

基于上述尝试,目前缺少阻止同时出现2个或以上合法字段的方法。最笨的方式是为每种语言定义类似{ langName: string }这样的类型并通过extends或联合类型使用,但这显然无法在其他情况通用。

通过官方现成的工具类型,由于都支持字面量和联合类型,无法筛选出只包含一个字段的类型。此时想到,是否可以定义一个包含全部字段但仅有一个字段正确有意义、其他字段无意义的类型?

最终,构造出下面这个PickOne工具类型:

type PickOne<T> = {
    [K in keyof T]: Record<K, T[K]> & Partial<Record<Exclude<keyof T, K>, undefined>>
}[keyof T]

测试代码如下:

type OneLang = PickOne<ICodingLangRating>

// 正确
const lang: OneLang = {
    java: 'good'
}

// 错误
const lang2: OneLang = {
    python: 'unknown'
}

// 错误
const lang3: OneLang = {
    java: 'good',
    go: 'good'
}

// 错误
const lang4: OneLang = {
    java: 123
}

最后,类型定义代码如下:

interface ICodingLangRating {
    java: string
    cpp: string
    go: string
    js: string
}

type Engineer = {
    name: string
    gender: 'male' | 'female'
    age: number
} & PickOne<ICodingLangRating>

使用了这个PickOne工具类型,使用时无需显式指定编程语言,甚至还能在其他类似场景中使用。

总结

到此这篇关于TypeScript开发小状况记录之选且只选一个的文章就介绍到这了,更多相关TypeScript选且只选一个内容请搜索本站以前的文章或继续浏览下面的相关文章希望大家以后多多支持本站!

实用工具推荐

电子包浆

绿化、压缩、噪点和虚拟水印,自由调节做旧程度。

使用 ›
图片压缩

选择画质和输出格式,预览压缩前后的大小。

使用 ›
相关文章