我有一个表单,它具有三个独立的实例,但其核心具有相同的数据,但具有一两个不同的属性。我想要一个基本表单类,它采用通用参数来缩小类型并删除一些不需要的控件,但我已经与 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 项目包含显示的所有代码。
对于遇到此问题的任何人,我不确定这是否可能,也不能实现我最初想要的。
我选择的是自定义表单生成器,它虽然丑陋,但确实允许我创建一个在某些表单上具有某些属性的类型化表单,而在其他表单上则不然。
这是代码示例:
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
,但我不在乎:)