我想创建一个相当灵活的类,称为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
跟踪requiredFields
和optionalFields
元素中的实际文字值,这意味着Model
在这些类型中可能是泛型的。所以可能类似于Model<R, O>
,其中R
是所有必需密钥的并集,O
是所有可选密钥的并合。
但您也希望Model<R, O>
实际上具有类型为R
和O
的键。这就是我们在使用常规的旧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
构造函数本身可能是有意义的。否则,您需要冗余地指定R
和O
:
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
返回的类构造函数关闭了传递给它的requiredFields
和optionalFields
变量
除了R
和O
现在是工厂函数上的泛型类型参数,而不是生成的类之外,这些类型与以前类似。作用域有点不同,所以(例如)ModelObject
在R
和O
中不需要是泛型的,因为R
和O
在该作用域中是已知的。此外,还有一些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
看起来不错!
游乐场链接到代码