Angular14 类型化FormArray提示Object is possibly 'undefined'
Angular 14 类型化FormArray嵌套访问属性报类型错误解决方案
问题现象
Angular 14项目中实现类型化响应式表单时,prices字段声明为FormArray<FormGroup<PriceFormModel>>类型后,模板中通过{{form.value.prices[i]?.price}}访问价格属性抛出类型错误,添加可选链运算符仍无法解决,相关截图如下:
相关代码
表单模型定义:
export interface ProductFormModel { id?: FormControl<id | null>; productId?: FormControl<id | null>; sku?: FormControl<string | null>; prices: FormArray<FormGroup<PriceFormModel>>; // prices: UntypedFormArray; stockSellerPercent: FormControl<number | null>; shopTax: FormControl<number | null>; titleRU: FormControl<string | null>; titleUA: FormControl<string | null>; descriptionRU: FormControl<string | null>; descriptionUA: FormControl<string | null>; smallDescriptionRU: FormControl<string | null>; smallDescriptionUA: FormControl<string | null>; bestSeller: FormControl<boolean | null>; hotSeller: FormControl<boolean | null>; productCategoryId: FormControl<id | null>; filters: UntypedFormArray; photos?: FormControl<any>; stockSeller: FormControl<boolean | null>; brandId: FormControl<id | null>; productStatus: FormControl<ProductStatusEnum | null>; }
prices表单数组子组模型:
export interface PriceFormModel { id: FormControl<id | null>; price: FormControl<number | null>; minQuantity: FormControl<number | null>; }
组件类核心代码:
@Component({ selector: 'app-add-new-item', templateUrl: './add-product.component.html', styleUrls: ['./add-product.component.scss'], }) export class AddProductComponent implements OnInit, AfterContentChecked { public form!: FormGroup<ProductFormModel>; public acceptedId: id = ''; public activateFileUpload = true; public titleUACtrl!: FormControl<string | null>; public titleRUCtrl!: FormControl<string | null>; public smallDescriptionUACtrl!: FormControl<string | null>; public smallDescriptionRUCtrl!: FormControl<string | null>; public descriptionRUCtrl!: FormControl<string | null>; public descriptionUACtrl!: FormControl<string | null>; public bestSellerCtrl!: FormControl<boolean | null>; public stockSellerCtrl!: FormControl<boolean | null>; public stockSellerPercentCtrl!: FormControl<number | null>; public hotSellerCtrl!: FormControl<boolean | null>; public shopTaxCtrl!: FormControl<number>; public productCategoryIdCtrl!: FormControl<id | null>; public brandCtrl!: FormControl<id | null>; public productStatusCtrl!: FormControl<ProductStatusEnum | null>; public shopTax = OPTIONS_SHOP_TAX; public submitted!: any; public value!: any; public optionsStockSellerPercent = OPTIONS_STOCK_SELLER_PERCENT; public isDisabledStockSellerPercent = true; public productStatuses: Array<productStatusModel> = PRODUCT_STATUSES_OPTIONS; constructor( private formBuilder: FormBuilder, private productServ: ProductService, private messageService: MessageService, private support: SupportService, private spinner: NgxSpinnerService, private cdr: ChangeDetectorRef, ) { this.createControls(); this.createForm(); } get f(): any { return this.form.controls; } get prices(): FormArray<FormGroup<PriceFormModel>> { return this.form.get('prices') as FormArray<FormGroup<PriceFormModel>>; } get filters(): UntypedFormArray { return this.form.get('filters') as UntypedFormArray; } ngOnInit(): void { } ngAfterContentChecked(): void { this.cdr.detectChanges(); } private createControls(): void { this.titleUACtrl = new FormControl<string | null>(null, [Validators.required]); this.titleRUCtrl = new FormControl<string | null>(null, [Validators.required]); this.productCategoryIdCtrl = new FormControl<id | null>(null, [Validators.required]); this.smallDescriptionRUCtrl = new FormControl<string | null>(null, [Validators.required]); this.smallDescriptionUACtrl = new FormControl<string | null>(null, [Validators.required]); this.descriptionRUCtrl = new FormControl<string | null>(null, [Validators.required]); this.descriptionUACtrl = new FormControl<string | null>(null, [Validators.required]); this.bestSellerCtrl = new FormControl<boolean | null>(false, [Validators.required]); this.stockSellerCtrl = new FormControl<boolean | null>(false, [Validators.required]); this.stockSellerPercentCtrl = new FormControl<number>({ value: 0, disabled: this.isDisabledStockSellerPercent, }, {nonNullable: true}); this.hotSellerCtrl = new FormControl<boolean>(false, {nonNullable: true}); this.shopTaxCtrl = new FormControl<number>(10, {nonNullable: true}); this.brandCtrl = new FormControl<id | null>(null, [Validators.required]); this.productStatusCtrl = new FormControl<ProductStatusEnum | null>(null, [Validators.required]); } private createForm(): void { this.form = this.formBuilder.group<ProductFormModel>({ id: new FormControl<id | null>(null), productStatus: this.productStatusCtrl, stockSellerPercent: this.stockSellerPercentCtrl, shopTax: this.shopTaxCtrl, titleUA: this.titleUACtrl, titleRU: this.titleRUCtrl, productCategoryId: this.productCategoryIdCtrl, smallDescriptionRU: this.smallDescriptionRUCtrl, smallDescriptionUA: this.smallDescriptionUACtrl, descriptionRU: this.descriptionRUCtrl, descriptionUA: this.descriptionUACtrl, prices: this.formBuilder.array([ this.formBuilder.group<PriceFormModel>({ id: new FormControl<id | null>(null), price: new FormControl<number | null >(null, [Validators.required]), minQuantity: new FormControl<number | null>(1, [Validators.required]) }), ]), bestSeller: this.bestSellerCtrl, stockSeller: this.stockSellerCtrl, hotSeller: this.hotSellerCtrl, brandId: this.brandCtrl, filters: this.formBuilder.array([]), }); console.log(this.form); } public addNewEl(): void { this.prices.push(this.formBuilder.group<PriceFormModel>({ id: new FormControl<id | null>(null), price: new FormControl<number | null>(null, [Validators.required]), minQuantity: new FormControl<number | null>(null, [Validators.required]), })); } }
问题根因
Angular 14严格类型响应式表单的泛型推导规则存在明确边界:
- 代码中定义的
ProductFormModel、PriceFormModel属于控件结构类型,作用是描述form.controls下各字段的控件实例类型,并非表单输出值的类型。 - 直接访问
form.value时,TypeScript不会递归将嵌套的FormArray、FormGroup类型拆包为普通对象值,会将form.value.prices的数组元素推导为控件实例类型,而非包含id/price/minQuantity属性的普通值对象。这种情况下访问.price属于访问类型上不存在的属性,TS会直接抛类型错误。可选链运算符仅能处理null/undefined的安全访问,无法解决属性不存在的类型校验问题。
修复方案
两种方案均可解决问题,优先选择第一种,符合Angular类型化表单最佳实践:
方案1:通过已定义的prices getter直接访问控件值(推荐)
组件类已经写好了类型正确的prices getter,模板中直接遍历该FormArray的控件集合,通过控件实例获取值即可,不需要绕路访问form.value,同时可以避免form.value无法获取禁用控件值的问题:
<!-- 原错误写法 --> <div *ngFor="let item of form.value.prices; let i = index"> {{ form.value.prices[i]?.price }} </div> <!-- 修改为正确写法 --> <div *ngFor="let priceGroup of prices.controls; let i = index"> {{ priceGroup.value.price }} <!-- 若表单中存在禁用控件需要取值,替换为getRawValue --> {{ priceGroup.getRawValue().price }} </div>
该方案不需要修改现有TS类型定义,类型校验完全通过。
方案2:独立定义表单值类型,做类型断言
如果业务场景必须通过form.value路径访问,单独定义表单值的普通对象类型,增加值访问getter即可:
- 新增值类型接口:
// 价格项值类型 export interface PriceValue { id: id | null; price: number | null; minQuantity: number | null; } // 整个表单值类型 export interface ProductFormValue { id?: id | null; productId?: id | null; sku?: string | null; prices: PriceValue[]; stockSellerPercent: number | null; shopTax: number | null; titleRU: string | null; titleUA: string | null; descriptionRU: string | null; descriptionUA: string | null; smallDescriptionRU: string | null; smallDescriptionUA: string | null; bestSeller: boolean | null; hotSeller: boolean | null; productCategoryId: id | null; filters: any[]; photos?: any; stockSeller: boolean | null; brandId: id | null; productStatus: ProductStatusEnum | null; }
- 组件类中新增值getter:
get formValue(): ProductFormValue { return this.form.getRawValue() as ProductFormValue; }
- 模板中替换访问路径为
formValue.prices[i]?.price即可通过类型校验。
注:现有带
FormControl/FormArray的控件结构接口不需要修改,作为FormGroup的泛型参数是完全正确的,问题仅出在值访问方式混淆了控件类型和值类型的边界。
内容的提问来源于stack exchange,提问作者Serhii Derkach
相关产品推荐
相关产品推荐

