缩小 Angular 中严格类型表单的联合

问题描述 投票:0回答:1

我有一个表单,它具有三个独立的实例,但其核心具有相同的数据,但具有一两个不同的属性。我想要一个基本表单类,它采用通用参数来缩小类型并删除一些不需要的控件,但我已经与 Typescript 斗争了几个小时,几乎要放弃并只使用非类型化表单。

我创建了一个较小的示例来显示问题:

export type FormOne = {
  name: AbstractControl<string | null>;
  email: AbstractControl<string | null>;
  age: AbstractControl<number | null>;
};

export type FormTwo = {
  name: AbstractControl<string | null>;
  email: AbstractControl<string | null>;
};

export type FormThree = {
  name: AbstractControl<string | null>;
  email: AbstractControl<string | null>;
  location: AbstractControl<string | null>;
};

export type BaseForm = FormOne | FormTwo | FormThree;

我想像这样定义我的基类:

@Directive()
export abstract class BaseFormComponent<T extends BaseForm> {
  private readonly _formBuilder = inject(FormBuilder);

  public readonly baseForm = this._formBuilder.group<T>({
    name: this._formBuilder.control<string | null>(''),
    email: this._formBuilder.control<string | null>(''),
    age: this._formBuilder.control<number | null>(0),
    location: this._formBuilder.control<string | null>(''),
  });
}

但遇到

TS2345
错误:

TS2345: Argument of type

{
  name: FormControl<string | null>;
  email: FormControl<string | null>;
  age: FormControl<number | null>;
  location: FormControl<string | null>; 
}

is not assignable to parameter of type T

{
  name: FormControl<string | null>;
  email: FormControl<string | null>;
  age: FormControl<number | null>;
  location: FormControl<string | null>;
}

is assignable to the constraint of type T, but T could 
be instantiated with a different subtype of constraint BaseForm.

这让我感到困惑,因为除了我定义的三个之外,没有其他 BaseForm 子类型,并且我对联合类型的理解是

BaseForm
将解析为:

{
  name: AbstractControl<string | null>;
  email: AbstractControl<string | null>;
  age?: AbstractControl<number | null>;
  location?: AbstractControl<string | null>;
}

这应该允许我在抽象类中定义的基本形式。 最终目标是拥有如下组件:

@Component({...})
export class FormTwoComponent extends BaseFormComponent<FormTwo> implements OnInit {
  public ngOnInit(): void {
    this.baseForm.removeControl('age');
    this.baseForm.removeControl('location');
  }
}

现在我意识到上面的组件有一个不符合给定的

T
的形式,直到
ngOnInit
运行,所以这可能会导致它自己的编译时问题,但到目前为止我还没有做到作为定义基类。

任何更精通 Typescript 的人都可以向我解释一下我是如何出错的,以及如何做到这一点(如果有的话)?

StackBlitz 项目包含显示的所有代码。

angular typescript angular-reactive-forms union-types type-narrowing
1个回答
0
投票

对于遇到此问题的任何人,我不确定这是否可能,也不能实现我最初想要的。

我选择的是自定义表单生成器,它虽然丑陋,但确实允许我创建一个在某些表单上具有某些属性的类型化表单,而在其他表单上则不然。

这是代码示例:

export class TypedFormBuilder<T extends Record<string, any> = {}> {
  private readonly _formBuilder = inject(FormBuilder);
  private readonly _form: FormGroup = this._formBuilder.group({}) as unknown as FormGroup<T>;

  public withName(name?: string) {
    this._form.addControl('name', this._formBuilder.control(name);

    return this as unknown as TypedFormBuilder<T & { name: string }>;
  }

  public withAge(age?: number) {
    this._form.addControl('age', this._formBuilder.control(age);

    return this as unknown as TypedFormBuilder<T & { age: number }>;
  }

  public build(): FormControl<T> {
    return this._form;
  }
}
const builder = new TypedFormBuilder(); // TypedFormBuilder<{}>

const form = builder 
  .withName('John') // TypedFormBuilder<{name: string}>
  .withAge(32) // TypedFormBuilder<{name: string} & {age: number}>
  .build(); //FormGroup<{name: string} & {age: number}>

form.controls. // intellisense suggests the controls

form.value // {name: 'John', age: 32}

您可以只创建一个通用

withControl
方法,并且仍然获得键入的形式:

public withControl<K extends string, V, NN extends boolean = false>(
  key: K,
  value: V,
  options: { nonNullable?: NN, validators?: ValidatorFn[] } = {}
) {
  const { nonNullable, validators } = options;

  this._form.addControl(key, this._formBuilder.control(value, {
    nonNullable,
    validators
  }));

  return this as unknown as TypedFormBuilder<T & { [key in K]: NN extends true ? V : V | null }>;
}

const form = builder
  .withControl('name', 'John', { nonNullable: true })
  .withControl('age', 32, { validators: [Validators.max(99)] })
  .build(); // FormGroup<{name: string} & {age: number | null}>

但我个人更喜欢显式的

withName
withAge
等,因为我的一些控件需要自定义验证器。

有些人可能称之为疯狂,有些人可能称之为作弊,因为我必须使用

unknown
,但我不在乎:)

© www.soinside.com 2019 - 2024. All rights reserved.