我有几十个带有不同字段的表单 - 复选框、上传、输入。
假设我需要一个类来处理表单 - 问题是一个类需要 inputForm、uploadForm。 其他表单只需要checkboxForm。如何以OOP方式处理呢? 我想出了配置参数,问题是自动完成功能有效,因此我可以访问未初始化的字段。 理想情况下,我不希望它们可见(作为类的客户端自动完成)。 我知道装饰器模式,但看起来我最终会得到一个带有几个包装器的混乱解决方案。
class InputForm {
// Define InputForm class here
}
class UploadForm {
// Define UploadForm class here
}
class CheckboxForm {
// Define CheckboxForm class here
}
interface Config {
includeInputForm: boolean;
includeUploadForm: boolean;
includeCheckboxForm: boolean;
}
class MultiForm {
inputForm?: InputForm;
uploadForm?: UploadForm;
checkboxForm?: CheckboxForm;
constructor(private config: Config) {
if (this.config.includeInputForm) {
this.inputForm = new InputForm();
}
if (this.config.includeUploadForm) {
this.uploadForm = new UploadForm();
}
if (this.config.includeCheckboxForm) {
this.checkboxForm = new CheckboxForm();
}
}
}
const config: Config = {
includeInputForm: true,
includeUploadForm: false,
includeCheckboxForm: true
};
const multiForm = new MultiForm(config);
class
声明必须具有静态已知成员(类实例实现为接口,并且接口必须具有静态已知成员)。所以你不可能有这样的代码
class MultiForm {
⋮
}
const mf1 = new MultiForm(config1);
const mf2 = new MultiForm(config2);
其中
mf1
和 mf2
具有不同的已知密钥。要么两者都具有 inputForm
属性,要么都没有。
所以问题的简单答案是“不,你不能这样做”。
但是,您可以描述以这种方式表现的类构造函数的类型,这意味着您可以编写常规类声明,然后断言持有对该类的引用的变量是所需的类型。所以它看起来像
class _MultiForm {
⋮
}
type MultiForm<T extends Config> = ⋯;
const MultiForm = _MultiForm as
new <T extends Config>(config: T) => MultiForm<T>;
const mf1 = new MultiForm(config1);
const mf2 = new MultiForm(config2);
这里我们有一个 generic
MultiForm<T>
类型别名,它应该有或缺少键,具体取决于作为 Config
类型参数传入的 T
的特定子类型。然后 mf1
和 mf2
可以有不同的键。
这对于某些用例来说可能已经足够了,尽管它需要大量的测试;任何需要类具有静态已知键的东西都会被破坏,所以即使上面的方法有效,下面的方法也不会:
class Oops<T extends Config> extends MultiForm<T> { } // error!
// --------------------------------> ~~~~~~~~~
// Base constructor return type 'MultiForm<T>' is not an
// object type or intersection of object types with
// statically known members.
但是,假设没问题,让我们继续前进。
我假设我们将保持您的类声明不变,除了将其重命名之外:
class _MultiForm {
inputForm?: InputForm;
uploadForm?: UploadForm;
checkboxForm?: CheckboxForm;
constructor(private config: Config) {
if (this.config.includeInputForm) {
this.inputForm = new InputForm();
}
if (this.config.includeUploadForm) {
this.uploadForm = new UploadForm();
}
if (this.config.includeCheckboxForm) {
this.checkboxForm = new CheckboxForm();
}
}
}
现在我们必须确定如何编写
_MultiForm
类和 MultiForm<T>
类型别名,无论如何,它可能会有点难看,因为我们必须将 {includeUploadForm: true, includeInputForm: false, includeCheckboxForm: true}
之类的内容翻译为“就像 _MultiForm
,除了 uploadForm
和 checkboxForm
属性是 required,而 inputForm
属性是 omissed。我们可以使用 Required
和 Omit
实用程序类型,但是它还是会很丑。
首先让我们定义我们想要保留/省略的 MultiForm
键的
union:
type FormKeys = "inputForm" | "uploadForm" | "checkboxForm";
对于其中任何一个都会有一个相应的
includeXxx
版本,我们可以使用 模板文字类型 来生成:
type IncludeForm<K extends string> = `include${Capitalize<K>}`
这让我们可以根据这些重新定义
Config
,尽管这不是必需的:
type Config = { [P in IncludeForm<FormKeys>]: boolean }
然后,对于
T
的给定子类型 Config
,让我们计算 PresentProps<T>
,我们想要存在且必需的 FormKeys
的子集:
type PresentProps<T extends Config> = {
[K in FormKeys]: T[IncludeForm<K>] extends true ? K : never
}[FormKeys]
这是一个分布式对象类型(在microsoft/TypeScript#47109中创造),我们使用映射类型和索引来获取其内容的并集。这意味着我们得到
T[IncludeForm<K>] extends true ? K : never
中所有 K
的 FormKeys
的并集。这意味着我们正在获取所有表单键 K
,其中 T
在键 IncludeForm<K>
的属性是 true
。
然后
MultiForm
终于是:
type MultiForm<T extends Config> =
Omit<_MultiForm, FormKeys> &
Required<Pick<_MultiForm, PresentProps<T>>>
我们首先省略
_MultiForm
中的所有表单键,然后根据需要添加回 true
中作为 T
出现的所有表单键。
让我们测试一下:
const config = {
includeInputForm: true,
includeUploadForm: false,
includeCheckboxForm: true
} as const;
const multiForm = new MultiForm(config);
multiForm.checkboxForm
// ^? (property) checkboxForm: CheckboxForm
multiForm.inputForm
// ^? (property) inputForm: InputForm
multiForm.uploadForm; // error!
// -----> ~~~~~~~~~~
// Property 'uploadForm' does not exist on type
现在,一切如你所愿。但请注意,我没有像你那样写
const config: Config =
。如果您将 注释 config
为 Config
类型,那么您将丢弃编译器拥有的有关该类型的任何更具体的信息。如果您希望编译器知道 includeInputForm
的类型为 true
而不是 boolean
,则无法进行注释。此外,您还需要诸如 const
断言之类的东西来跟踪文字 true
和 false
,因为它们也往往会扩展到 boolean
。
再进行一次测试:
const mf2 = new MultiForm({
includeUploadForm: true,
includeCheckboxForm: false,
includeInputForm: false
});
mf2.uploadForm // okay
mf2.inputForm // error
看起来也不错。请注意,通过将配置内联声明为对象文字,我们可以避免对
as const
的全部需求。
所以,它有效。这值得么?我想说可能不是。它对抗 TypeScript 类型系统。它必须绕过正常的类声明,然后还必须跳过类型环。与其拥有
includeXXX
,为什么不直接使用单个 forms
属性(它是 {inputForm?: InputForm, checkboxForm?: CheckboxForm, uploadForm?: UploadForm}
的某种子类型)来初始化类并使该类通用?是的,您需要写 multiForm.forms.inputForm
而不是 multiForm.inputForm
,但现在 MultiForm<T>
始终具有 forms
属性,因此 MultiForm
的键是已知的。而且您不必费力将 includeXXX
转换为其他内容,您只需自己初始化属性即可。或者也许这行不通,而且从技术上讲,这超出了所提出问题的范围。但在使用之前,您可能应该确保您的用例实际上需要这样的拜占庭解决方案。