【问题标题】:How do I create a generic factory method that access static members of the class type it's creating如何创建访问它正在创建的类类型的静态成员的通用工厂方法
【发布时间】:2019-11-15 14:30:14
【问题描述】:

我正在使用 Typescript 将 REST API 端点编写为 Firebase 上的函数,并且这些方法都遵循类似的模式:检查 request.body,从该主体数据中提取适当的数据,将其放入一个强-类型化对象,使用该对象通过一些数据访问代码将数据推送到数据库。在多次编写相同的基本数据提取逻辑来处理 request.body 之后,我认为必须有一种方法来抽象这项工作。我对此有三个要求:(1)该方法应该可以从我的任何数据模型的 request.body 中提取数据。 (2) 数据模型应该是完全自描述的,这样它们不仅描述了数据应该具有的属性,而且当需要一组特定的属性时它们可以关联起来。 (3) 该方法应该能够从数据模型中分辨出需要哪些属性,并对通过 request.body 传递的数据进行一些验证。

作为#2 的示例,模型是自描述的:例如,当我创建一个新的数据记录时,我不需要 ID,因为如果它不存在,我可以创建它在函数中并将其传回。另一方面,在这种情况下需要 的“名称”属性。相比之下,update 方法需要记录 ID(因此它知道要更新哪条记录),但 不需要 需要“名称”,除非那是实际存在的修改的。

我的方法是(1)在一个单独的类上使用静态工厂方法,该方法采用需要创建的数据模型的类类型;预期的操作(即 createreadupdatedelete);和请求正文。 (2) 一组数据模型类,基本上只描述数据并在需要的地方包含一点验证逻辑,还包括字段名称和相关要求值的(静态)列表(存储为四位,其中每个位置代表一个四个 CRUD 操作中的一个。) (3) 一个通用接口,以便静态工厂方法知道如何处理不同的数据对象以获取那些字段名称和使用标志。

这是我的静态工厂方法:

static create<T extends typeof DataObjectBase>(cls: { new(...args: any[]): T; }, intendedOperation: number, requestBody: any) : T {
        let dataObject : T = null;
        const sourceData = {};
        const objFields = cls.fieldNames;
        const flagCollection = cls.requiredUseFlags();
        const requiredFields = flagCollection.getFieldsForOperation(intendedOperation);
        if (requestBody) {
            // parse the request body
            // first get all values that are available and match object field names
            const allFields = Object.values(objFields); // gets all properties as key/value pairs for easier iteration
            // iterate through the allFields array
            for (const f in allFields) {
                if (requestBody.hasOwnProperty(f)) {
                    // prop found; add the field to 'sourceData' and copy the value from requestBody
                    sourceData[f] = requestBody[f];
                } else if (requiredFields.indexOf(f)>-1) {
                    // field is required but not available; throw error
                    throw new InvalidArgumentError(`${cls}.${f} is a required field, but no value found for it in request.body.`, requestBody);
                }
            }
            dataObject = (<any>Object).assign(dataObject, sourceData);
        } else {
            throw new ArgumentNullError('"requestBody" argument cannot be null.', requestBody);
        }
        return new cls();
    }

这是一个数据模型类的示例:

export class Address extends DataObjectBase {
    constructor(
        public id         : string,
        public street1    : string,
        public street2    : string = "",
        public city       : string,
        public state      : string,
        public zip        : string) {
        // call base constructor
        super();
    }

    static fieldNames = {
        ID      = "id",
        STREET1 = "street1",
        STREET2 = "street2",
        // you get the idea...
    }

    static requiredUseFlags() {
        ID = READ | UPDATE | DELETE,
        STREET1 = 0,
        // again, you get the idea...
        // CREATE, READ, UPDATE, DELETE are all bit-flags set elsewhere
    }
}

我希望能够像这样调用上面的create 方法:

const address = create<Address>(Address, CREATE, request.body);

最初,我尝试了这样的签名:

static create<T extends typeof DataObjectBase>(cls: T, intendedOperation: number, requestBody: any) : T

但是,当我这样做时,我收到一个错误,“地址是一种类型,但被用作一个值。”一旦我将其更改为上面的内容,我就不再收到该错误并开始收到 Property 'fieldNames' does not exist on type 'new (...args: any[]) =&gt; T'

注意:我也尝试过使用两个接口来描述(第一个)实例方法和(第二个)静态方法的技巧,然后让静态接口扩展实例接口,然后基类实现静态接口等,如hereherehere等。这也没有让我到达那里。

我当然愿意承认我很可能过度设计了这一切,我很乐意接受其他更简单的建议。但我认为必须有一种方法来完成我想要的,并且避免一遍又一遍地编写相同的基本请求体解析代码。

【问题讨论】:

  • DataObjectBase 是抽象的吗?
  • 这不是抽象的。我在它的所有子类中都使用了许多与数据相关的通用方法。
  • 无论哪种方式,下面的解决方案都应该有效

标签: typescript typescript-generics


【解决方案1】:

您可以在静态方法中使用this 来引用当前类(使您可以编写new this() 来创建该类的实例)。

关于以能够构造对象和访问静态的方式键入 this,最简单的解决方案是使用您定义的构造函数签名,并通过使用与Pick&lt;typeof DataObjectBase, keyof typeof DataObjectBase&gt;。这将保留静态成员,但删除基类的任何构造函数签名。

同样T 应该扩展DataObjectBase(实例类型)而不是typeof DataObjectBase(类的类型)

type FieldsForOperation = {
    getFieldsForOperation(intendedOperation: number): string[]
}

class DataObjectBase {
    static fieldNames: Record<string, string>
    static requiredUseFlags():FieldsForOperation { return null!; }
    static create<T extends DataObjectBase>(
        this: (new (...a: any[]) => T) & 
            Pick<typeof DataObjectBase, keyof typeof DataObjectBase>, 
        intendedOperation: number, 
        requestBody: any
    ): T {
        let dataObject : T = null;
        const sourceData = {};
        const objFields = this.fieldNames;
        const flagCollection = this.requiredUseFlags();
        // rest of code
        return new this();
    }
}

export class Address extends DataObjectBase {
    constructor(
        public id         : string,
        public street1    : string,
        public street2    : string = "",
        public city       : string,
        public state      : string,
        public zip        : string) {
        // call base constructor
        super();
    }

    static fieldNames = {
        "": ""
    }

    static requiredUseFlags(): FieldsForOperation {
        return null!;
    }
}

Address.create(0, {})

注意:仅修复 TS 不会对过度工程部分发表意见?

【讨论】:

  • 人民冠军
  • @titian 非常感谢您的回答!现在通过它。同时,我对这一行有疑问:typescript static requiredUseFlags():FieldsForOperation { return null!; } "null!" 是什么意思?做?我已经搜索过了,找不到任何有用的东西。
  • @bubba 只是实现的占位符。我正在返回null,但在严格的null 下检查null 不能分配给FieldsForOperation,所以我断言null 不是null 与非空断言(!) 沉默错误
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2011-12-12
  • 1970-01-01
  • 2015-06-26
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2011-01-18
相关资源
最近更新 更多