为何TypeScript在此条件类型中推断出Array类型?
问题重现
你定义了以下条件类型:
type ComplexType<T> = { [key in keyof T]: T[key] extends { type: unknown schema?: unknown } ? | (T[key]['schema'] extends object ? ComplexType<T[key]['schema']> : never) | TypeFromConstructor<T[key]['type']> : TypeFromConstructor<T[key]> | undefined } type TypeFromConstructor<T> = T extends StringConstructor ? string : T extends NumberConstructor ? number : T extends BooleanConstructor ? boolean : T extends DateConstructor ? Date : T extends ArrayConstructor ? never : T extends ObjectConstructor ? never : unknown
将其应用于以下对象时:
const schema = { test: { type: Array, schema: [{ type: Object, schema: { id: String, }, }], }, } type testType = ComplexType<typeof schema>
实际得到的类型是{ test: Array<{id: string}> },但预期结果为{ test: { '0': {id: string}} },同时存在两个疑问:
- 未检查schema的类型是否为数组,为何会推断出Array类型?
- 类型中未包含length等数组默认属性,是否与JavaScript数组本质是对象有关?
问题原因分析
1. 数组类型推断的来源
TypeScript对映射类型作用于数组/元组有特殊处理逻辑:当映射类型遍历数组类型的键时,不会生成带有数字索引的对象,而是自动转换为泛型数组类型(Array<ElementType>)。
在你的代码中,T[key]['schema']是数组字面量[{type: Object, schema: {id: String}}],它属于object类型,因此会执行ComplexType<T[key]['schema']>。此时TypeScript触发了数组的特殊映射逻辑,将结果转换为Array<ComplexType<typeof schema.test.schema[0]>>,而非带有'0'索引的对象。
另外,TypeFromConstructor<ArrayConstructor>返回never,所以联合类型中这部分被忽略,最终结果只保留了数组类型的分支。
2. 数组属性缺失的原因
你看到的Array<{id: string}>类型本身是包含length、push等属性的,但如果是预期的{ '0': {id: string} }这种对象类型,确实不会包含这些属性——因为它只是一个普通对象,仅拥有显式定义的数字索引键,和数组原型无关。
而TypeScript生成的数组类型之所以没显式展示length等属性,是因为这些属性属于Array接口的默认成员,不会在类型别名的展开结果中逐条列出,但实际类型检查时是存在的。
解决方案:强制将数组转为索引对象类型
要让ComplexType处理数组时生成带有数字索引的对象,需要在映射逻辑中显式判断并处理数组类型,绕过TypeScript的自动转换:
type ComplexType<T> = { [key in keyof T]: T[key] extends { type: unknown schema?: unknown } ? | (T[key]['schema'] extends infer S ? S extends any[] ? { [K in keyof S]: ComplexType<S[K]> } // 对数组做索引映射,生成对象类型 : S extends object ? ComplexType<S> : never : never) | TypeFromConstructor<T[key]['type']> : TypeFromConstructor<T[key]> | undefined } type TypeFromConstructor<T> = T extends StringConstructor ? string : T extends NumberConstructor ? number : T extends BooleanConstructor ? boolean : T extends DateConstructor ? Date : T extends ArrayConstructor ? never : T extends ObjectConstructor ? never : unknown
修改后,testType会被推断为:
type testType = { test: { '0': { id: string; }; }; }
完全符合你的预期。
内容的提问来源于stack exchange,提问作者D'witt

