TypeScript如何实现数组子元素按属性值单独推导关联属性类型
问题场景
设想如下场景:对象的某一个属性的类型,依赖于同对象下另一个属性的取值。
举例:假设有一颗星球上仅有两个国家,若孩子出生在onename land,则其姓名应为string类型;若出生在twonames land,则其姓名应为两个字符串组成的元组类型。
const iPeople: { nationality: 'onename land' | 'twonames land', name: string | [string, string] }[] = [ { nationality: 'onename land', name: 'Jon' }, { nationality: 'twonames land', name: ['Jon', 'Snow'] } ]
将该场景泛化得到通用示例:
const iSomeThings: { a: 'X' | 'Y', b: string | [string, string] }[] = [ { a: 'X', b: 'lorem' }, { a: 'Y', b: ['lorem', 'ipsum'] } ]
在上述iSomeThings数组的两个对象中,属性b的推导类型均为string | [string, string],但我们期望可以根据属性a的取值,让每个对象的b属性单独对应string或[string, string]类型:
iSomeThings[0].b // 推导类型为 "string | [string, string]",但期望是 "string",因为a的值为'X' iSomeThings[1].b // 推导类型为 "string | [string, string]",但期望是 "[string, string]",因为a的值为'Y'
核心需求:让属性b的类型依赖于属性a的取值。
即当属性a取值为'X'时,属性b的类型应为string;当属性a取值为'Y'时,属性b的类型应为[string, string]元组。
基础实现尝试
我们首先可以通过泛型条件类型实现基础的类型关联:
type A = 'X' | 'Y' type B<T extends A> = T extends 'X' ? string : T extends 'Y' ? [string, string] : never interface ISomeThing<T extends A> { a: T b: B<T> } class SomeThing<T extends A> implements ISomeThing<T> { a: T b: B<T> constructor(private _iSomething: ISomeThing<T>) { this.a = this._iSomething.a this.b = this._iSomething.b } }
符合预期的使用场景
指定明确字面量泛型参数时,类型推导符合预期:
const someThing1: SomeThing<'X'> = new SomeThing<'X'>({ a: 'X', b: 'lorem' }) const someThing2: SomeThing<'Y'> = new SomeThing<'Y'>({ a: 'Y', b: ['lorem', 'ipsum'] }) someThing1.b // ✅ b的类型为 'string' someThing2.b // ✅ b的类型为 '[string, string]'
不符合预期的使用场景
- 传入联合泛型参数A时,类型推导不符合预期:
const someThing1: SomeThing<A> = new SomeThing<A>({ a: 'X', b: 'lorem' }) const someThing2: SomeThing<A> = new SomeThing<A>({ a: 'Y', b: ['lorem', 'ipsum'] }) someThing1.b // ❌ b的类型为 'string | [string, string]',期望为 'string' someThing2.b // ❌ b的类型为 'string | [string, string]',期望为 '[string, string]'
- 通过map方法遍历数组、尝试从每个元素推导泛型参数时,类型推导不符合预期:
const iSomeThings: ISomeThing<A>[] = [{ a: 'X', b: 'lorem' }, { a: 'Y', b: ['lorem', 'ipsum'] }] const someThings: SomeThing<A>[] = iSomeThings.map(iSomeThing => new SomeThing<typeof iSomeThing.a>(iSomeThing)) someThings[0].b // ❌ b的类型为 'string | [string, string]',期望为 'string' someThings[1].b // ❌ b的类型为 'string | [string, string]',期望为 '[string, string]'
此处需要注意泛型参数<typeof iSomeThing.a>的推导逻辑:
<typeof iSomeThing.a>
⚠️ 题外话:如果移除typeof关键字,泛型参数传入的会是运行时值(例如'X')而非类型,会触发TS类型报错,难道需要基于a的属性值单独创建对应的类型吗?
⚠️ 前提说明:当前场景下a的类型被标注为联合类型A(即'X' | 'Y'),因此b的类型也会被推导为联合类型string | [string, string],为了聚焦核心问题,我们先固定该前提。
待解决问题
目前访问someThings[n]时,TypeScript会使用数组声明的父级泛型类型,忽略每个子元素的具体类型,请问应当如何实现数组内子元素的类型单独推导,使得访问数组元素时可以获得对应准确的关联属性类型?
内容的提问来源于stack exchange,提问作者Christopher Kiessling

