如何在 Typescript OOP 中使用方法/属性有条件地扩展类

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

我有几十个带有不同字段的表单 - 复选框、上传、输入。

假设我需要一个类来处理表单 - 问题是一个类需要 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);
typescript oop
1个回答
0
投票

在 TypeScript 中,

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
转换为其他内容,您只需自己初始化属性即可。或者也许这行不通,而且从技术上讲,这超出了所提出问题的范围。但在使用之前,您可能应该确保您的用例实际上需要这样的拜占庭解决方案。

Playground 代码链接

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