如何创建一个通用工厂方法来访问它正在创建的 class 类型的静态成员

How do I create a generic factory method that access static members of the class type it's creating

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

传递的数据进行一些验证

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

我的方法是 (1) 在单独的 class 上使用静态工厂方法,该方法采用 class 类型作为需要创建的数据模型;预期的操作(即 createreadupdatedelete);和请求正文。 (2) 一组数据模型 classes,基本上只是描述数据并在需要时包含一些验证逻辑,但也包含字段名称和相关需求值的(静态)列表(存储为四位,其中每个位置代表四个 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();
    }

这是一个数据模型示例 class:

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

然而,当我这样做时,我收到一个错误 "Address is a type but is being used as a value." 一旦我将其更改为上面的内容,我就不再收到该错误并开始收到 Property 'fieldNames' does not exist on type 'new (...args: any[]) => T'

注意:我也尝试过使用两个接口来描述(第一个)实例方法和(第二个)静态方法,然后让静态接口扩展实例接口,并且基础 class 实现静态接口等,如所描述的 here, here, and here。这也没有完全让我到达那里。

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

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

关于以既能构造对象又能访问静态的方式键入它,最简单的解决方案是使用您定义的构造函数签名,然后使用与 Pick<typeof DataObjectBase, keyof typeof DataObjectBase> 相交。这将保留静态成员,但删除基 class.

的任何构造函数签名

另外 T 应该扩展 DataObjectBase(实例类型)而不是 typeof DataObjectBase(class 的类型)

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 不会对 over-engineering 部分发表意见

这也可以用 jsdoc 记录吗?

   /**
     * @template {DataObjectBase} T
     * @this {new (...args: any[]) => T}
     * @returns {Promise<T>}
     */
    static create (
        intendedOperation: number, 
        requestBody: any
    ) {...}

create的方法签名被vscode理解,但是调用时,DataObjectBaseAddress不理解this的多态性。