使用具有特定字段的typescript创建类



我想创建一个相当灵活的类,称为Model,比如:

export class Model {
_required_fields: Array<string> = [];
_optional_fields?: Array<string> = [];
constructor(params: Dictionary<string> = {}) {
// make sure all required fields are in the params obj
}
set(params: Dictionary<string>){
// make sure only required or optional fields are present
this.all_fields.forEach(key => {
this[key] = params[key];
});
}
get all_fields(): Array<string> {
return [...this._required_fields,...this._optional_fields];
}
get required_fields() {
return this._required_fields;
}
}

它的子项将定义必需字段和可选字段,我将其缩短,因为我在set方法中进行了一些错误检查。例如:

export class User extends Model {
static REQUIRED_FIELDS = ['username'];
static OPTIONAL_FIELDS = ['email'];
get required_fields() {
return (this._required_fields?.length==0) ? User.REQUIRED_FIELDS : [];
}
static get ALL_FIELDS() {
return [...User.REQUIRED_FIELDS, ...User.OPTIONAL_FIELDS];
}
constructor(params: Dictionary<string> = {}) {
super(params);
}
}

我有一个版本的User,其中包含以下字段:

username: string;
email: string;

但我希望能够定义字段,以便set函数可以采用Dictionary并填充字段,如图所示。

我在行收到打字错误No index signature with a parameter of type 'string' was found on type 'Model'.

this[key] = params[key];

我意识到这一点是因为我需要在Model内部定义一个类似于:[key: string]: string;的字段。

这似乎有两种可能性:

方法1:在User内部,定义每个字段,在set(User)内部显式执行

user.username = params.username;
user.email = params.email;

然而,我必须为Model的所有子代重复这一点,并且我对此有一些错误检查,我想自动化一点。

方法2:或者,我可以保留具有通用字段的Model

[key: string]: string;

然后set会按原样工作,但不会有能力做user.username,但可以做user['username']

摘要

到目前为止,我已经完成了方法1,并且有大量重复的代码,因为我需要为Model的每个子级显式地完成所有字段。(事实上,我有两个以上的领域)。这并不令人满意——我似乎可以在Model课堂上比每个孩子写得更紧凑。

方法2似乎绕过了typescript的大部分强类型,所以尽管代码更紧凑,但看起来并不好。

问题

有没有什么方法可以让我把打字脚本的强类型与方法1 的灵活性结合起来

看起来您希望Model跟踪requiredFieldsoptionalFields元素中的实际文字值,这意味着Model在这些类型中可能是泛型的。所以可能类似于Model<R, O>,其中R是所有必需密钥的并集,O是所有可选密钥的并合。

但您也希望Model<R, O>实际上具有类型为RO键。这就是我们在使用常规的旧class语句时遇到麻烦的地方。类和接口要求它们的键对于编译器来说是静态已知的。它们不能是动态的并且稍后填写:

class Foo<T extends object > implements T {} // error!
// -----------------------------------> ~
// A class can only implement an object type or intersection 
// of object types with statically known members.

所以我们需要解决这个问题。


描述这样的类构造函数类型很容易。它应该看起来像这个

type ModelConstructor = new <R extends string, O extends string>(
params: ModelObject<R, O>
) => ModelObject<R, O> & {
set(params: Partial<ModelObject<R, O>>): void,
all_fields: Array<R | O>;
required_fields: R[];
}
type ModelObject<R extends string, O extends string> =
Record<R, string> & Partial<Record<O, string>>;

因此,CCD_ 27具有采用CCD_ 29类型的CCD_ 28的构造签名。CCD_ 30是具有必需密钥R和可选密钥O的对象类型,并且其值都是string。构造的实例也是一个ModelObject<R, O>,它与Model类中的一组适当类型的属性和方法相交。

不过,在继续讨论之前,考虑一下如何将Model子类化可能会很有用。由于您希望User的每个实例都具有相同的必需字段和可选字段,因此将Model设置为类工厂函数而不是class构造函数本身可能是有意义的。否则,您需要冗余地指定RO:

class AnnoyingUser extends Model<"username", "email"> {
_required_fields = ["username"] // redundant
_optional_fields = ["email"] // redundant
}

只写可能要好得多

class User extends Model(["username"], ["email"]) { }

其中CCD_ 42产生具有那些已经就位的字段的类构造函数。这取决于你是否想这样做。我将假设工厂功能是可以接受的,并继续使用它


所以这是工厂功能:

export const Model = <R extends string, O extends string>(
requiredFields: R[], optionalFields: O[]
) => {
type ModelObject =
Record<R, string> & Partial<Record<O, string>> extends
infer T ? { [K in keyof T]: T[K] } : never;
class _Model {
_required_fields: Array<R> = requiredFields;
_optional_fields?: Array<O> = optionalFields;
constructor(params: ModelObject) {
this.set(params);
}
set(params: Partial<ModelObject>) {
this.all_fields.forEach(key => {
(this as any)[key] = (params as any)[key];
});
}
get all_fields(): Array<R | O> {
return [...this._required_fields,
...this._optional_fields ?? []];
}
get required_fields() {
return this._required_fields;
}
}
return _Model as any as new (params: ModelObject) =>
ModelObject & {
set(params: Partial<ModelObject>): void,
all_fields: Array<R | O>;
required_fields: R[];
} extends infer T ? { [K in keyof T]: T[K] } : never;
}

注意,从Model返回的类构造函数关闭了传递给它的requiredFieldsoptionalFields变量

除了RO现在是工厂函数上的泛型类型参数,而不是生成的类之外,这些类型与以前类似。作用域有点不同,所以(例如)ModelObjectRO中不需要是泛型的,因为RO在该作用域中是已知的。此外,还有一些extends infer T ? {[K in keyof T]: T[K]}: never类型,这些类型只是为了要求编译器将Record<"username", string> & Partial<Record<"email", string>>之类的东西扩展为外观更好的{username: string; email?: string}类型。

还要注意,由于编译器不能将class表示为具有动态属性,因此我需要使用类型断言来告诉编译器,它可以将返回的构造函数视为正确的类型。

一旦你使用了一个工厂函数,你就可以决定进一步重构;也许您希望所需/可选字段仅为返回类构造函数的static属性,因为它们对于类的每个实例都应该相同。不过我不打算麻烦你。只是一个想法。


让我们测试一下。首先,让我们看看当我们调用Model:时会产生什么结果

const UserModel = Model(["username"], ["email"]);
/* const UserModel: new (params: {
username: string;
email?: string | undefined;
}) => {
username: string;
email?: string | undefined;
set: (params: Partial<{
username: string;
email?: string | undefined;
}>) => void;
all_fields: ("username" | "email")[];
required_fields: "username"[];
} */

看起来很合理,我认为这是你想要的。现在我们可以定义User:

export class User extends Model(["username"], ["email"]) {
// static REQUIRED_FIELDS = ['username'];
// static OPTIONAL_FIELDS = ['email'];
}

我注释掉的那些static属性来自您的示例,但它们对于我的解决方案来说不是必需的。如果你想要它们,可以随意添加,但我不知道它们有什么用途。

让我们测试一下:

const u = new User({ username: "alice" });
console.log(u.required_fields) // ["username"]
console.log(u.all_fields) // ["username", "email"]
u.set({ email: "foo@example.com" })
console.log(u.email) // foo@example.com

看起来不错!

游乐场链接到代码

相关内容

  • 没有找到相关文章

最新更新