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

TypeScript映射类型中枚举数组解析为枚举成员数组联合类型的问题

TypeScript映射类型处理枚举数组的类型问题

我正在尝试实现一个TypeScript映射类型,用来从给定的模型类型生成Angular表单组类型。目前已经实现了检测数组属性并转为FormArray<T>的逻辑,但遇到了枚举数组的类型映射问题:当属性类型为MyEnum[]时,解析后的类型变成了MyEnum.A[] | MyEnum.B[],导致无法向数组中添加不同的枚举值。

简化问题复现代码

enum Environment{
  "EN" = "en",
  "US" = "us",
}

interface MyDto {
  environments: Environment[];
}

export type FormControlsOf<T> = {
  // 检查属性是否为数组
  [TPropertyKey in keyof T]-?: T[TPropertyKey] extends Array<infer TArray>
  // 检查TArray是否为Record类型,以此决定是转为FormControl还是FormGroup(真实代码逻辑)
  ? TArray extends Record<any, any> ?
    // 示例中忽略Record类型
    never 
    // 将属性映射为数组;真实代码中会转为FormArray<FormControl<TArray>>
    : TArray[]
  // 示例中忽略非数组类型
  : never;  
}

const q: FormControlsOf<MyDto> = {
  environments: [Environment.EN, Environment.US]
  // environments的实际类型是 (Environment.EN[] | Environment.US[])
};

// 错误提示:
// Type 'Environment[]' is not assignable to type 'Environment.EN[] | Environment.US[]'.
//  Type 'Environment[]' is not assignable to type 'Environment.EN[]'.
//    Type 'Environment' is not assignable to type 'Environment.EN'.
//      Type 'Environment.US' is not assignable to type 'Environment.EN'.(2322)
// input.tsx(8, 3): 预期类型来自属性'environments',该属性在类型'FormControlsOf<MyDto>'上声明

问题补充说明

如果移除第13行对TArray的检查,environments的类型可以正确识别为Environment[],但我需要这个检查来判断属性应该转为FormArray<FormGroup>还是FormArray<FormControl>。

经@wonderflame和@Robby Cornelissen指出,分布式条件类型是问题的根源。

完整的Angular表单组映射类型代码

/**
 * 该映射类型用于生成对应模型类型的表单组表示类型
 *
 * 示例:
 * interface MyDto {
 *    prop1: string;
 *    prop2: string[];
 * }
 *
 * FormGroupOf<MyDto> 会被解析为:
 * {
 *    prop1: FormControl<string>;
 *    prop2: FormArray<FormControl<string>>;
 * }
 *
 * 这样可以定义完全类型化的表单字段
 *
 * class TestClass {
 *
 *    public form: FormGroupOf<MyDto>;
 *
 *    constructor() {
 *      this.form = new FormGroup({
 *        prop1: new FormControl(""),
 *        prop2: new FormArray<FormControl<string>>([]),
 *      })
 *    }
 * }
 *
 * 这种方式在模板和表单组构造过程中都能获得代码补全
 *
 * 对于复杂属性类型,支持递归处理:
 * interface MyDto2 {
 *    prop1: {
 *      innerProp: string;
 *    };
 * }
 *
 * FormGroupOf<MyDto2> 会被解析为:
 * {
 *    prop1: FormGroup<{
 *      innerProp: FormControl<string|null>;
 *    }>;
 * }
 */
export type FormGroupOf<T> = FormGroup<FormControlsOf<T>>;

export type FormControlsOf<T> = {
  [TPropertyKey in keyof T]-?: T[TPropertyKey] extends
    | (infer TArray)[]
    | undefined
    ? [TArray] extends [string | number | boolean | undefined]
      ? FormArray<FormControl<TArray>>
      : FormArray<FormGroupOf<TArray>>
    : T[TPropertyKey] extends string | number | boolean | undefined
    ? FormControl<T[TPropertyKey] | null>
    : FormGroupOf<T[TPropertyKey]>;
};

解决方案:避免分布式条件类型的影响

问题核心在于,当TArray是枚举这类联合类型时,TArray extends XXX形式的条件类型会触发分布式特性——将联合类型的每个成员单独代入判断,最终返回成员类型数组的联合(如Environment.EN[] | Environment.US[]),而非我们期望的联合类型数组(Environment[])。

要解决这个问题,只需通过元组包裹的方式禁用条件类型的分布式特性:

修改后的简化版代码

export type FormControlsOf<T> = {
  [TPropertyKey in keyof T]-?: T[TPropertyKey] extends Array<infer TArray>
  // 用元组包裹TArray,阻止分布式条件类型展开
  ? [TArray] extends [Record<any, any>] ?
    never 
    : TArray[]
  : never;  
}

// 此时 FormControlsOf<MyDto>['environments'] 的类型为 Environment[]
const q: FormControlsOf<MyDto> = {
  environments: [Environment.EN, Environment.US] // 无类型错误
};

修改后的完整Angular表单组映射类型

将同样的逻辑应用到完整代码中,确保所有条件判断都避免分布式展开:

export type FormControlsOf<T> = {
  [TPropertyKey in keyof T]-?: T[TPropertyKey] extends
    | (infer TArray)[]
    | undefined
    // 用元组包裹TArray,避免拆分枚举联合类型
    ? [TArray] extends [string | number | boolean | undefined]
      ? FormArray<FormControl<TArray>>
      : FormArray<FormGroupOf<TArray>>
    : [T[TPropertyKey]] extends [string | number | boolean | undefined]
    ? FormControl<T[TPropertyKey] | null>
    : FormGroupOf<T[TPropertyKey]>;
};

修改后,枚举数组的类型会被正确识别为联合类型的数组,而非数组类型的联合,即可正常向数组中添加不同的枚举值。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.22 10:27:04