英文:
How to make all class properties optional in Typescript
问题
<!-- 这是一种方法来做到这一点:-->
<!-- 开始代码段: ts hide: false console: true babel: false -->
<!-- 语言: lang-js -->
interface CreateDTO {
id: string;
name: string;
price: number;
}
interface UpdateDTO extends Partial<CreateDTO> {}
<!-- 结束代码段 -->
但是,使用类似于这样的类:
<!-- 开始代码段: ts hide: false console: true babel: false -->
<!-- 语言: lang-js -->
class CreateDTO {
id: string;
name: string;
price: number;
}
class UpdateDTO extends Partial<CreateDTO> {}
<!-- 结束代码段 -->
问题在于我必须使用类而不是接口,因为我必须添加装饰器来进行验证。
英文:
There is a way to make this:
<!-- begin snippet: ts hide: false console: true babel: false -->
<!-- language: lang-js -->
interface CreateDTO {
id: string;
name: string;
price: number;
}
interface UpdateDTO extends Partial<CreateDTO> {}
<!-- end snippet -->
But using classes, something like this:
<!-- begin snippet: ts hide: false console: true babel: false -->
<!-- language: lang-js -->
class CreateDTO {
id: string;
name: string;
price: number;
}
class UpdateDTO extends Partial<CreateDTO> {}
<!-- end snippet -->
The problem is that I have to use classes instead of interfaces because I have to add decorators for the validations.
答案1
得分: 2
这段代码涉及转换一个类定义(CreateDTO
)为另一个类定义(UpdateDTO
)的问题,首先,候选类定义CreateDTO
未能初始化其必需属性。如果启用--strictPropertyInitialization
编译选项,会导致编译错误。修复这些错误需要考虑如何实际构建类的实例,例如通过传递属性的构造函数参数。不能使用class UpdateDTO extends Partial<CreateDTO>
,因为Partial
仅作用于类型,需要扩展CreateDTO
类构造函数值。可以编写一个Partial
function来扩展泛型类型T
的类构造函数为Partial<T>
,然后应用它。但这并没有真正解决问题,因为新类仍然需要这些构造函数参数,因为它只是从CreateDTO
派生而来。UpdateDTO
类的要点是它不需要这些参数。从概念上讲,类UpdateDTO
实际上并不是从类CreateDTO
派生出来的。它们更像是从您的接口定义CreateDTO
和Partial<CreateDTO>
派生出来的两个单独的类。因此,将这看作将接口定义转换为类定义可能更有意义。可以创建一个工厂函数,该函数接受一个对象类型定义并生成一个类构造函数,该构造函数接受该类型的构造函数参数并生成该类型的类实例。实现使用Object.assign()
方法将输入参数传播到正在构建的实例中。然后,可以使用它来创建CreateDTO
和UpdateDTO
类。唯一可能更改的是,在对象类型没有必需属性的情况下,如果{}
是可接受的输入,不需要强制类构造函数接受参数。可以在DTOClass
中使用条件类型使参数变为可选,如果{}
是可接受的输入。现在您可以编写const u = new UpdateDTO();
。【Playground链接到代码】
英文:
It's problematic to think of this as transforming one class definition (CreateDTO
) into another class definition (UpdateDTO
). First, your candidate class definition
class CreateDTO {
id: string; // error!
name: string; // error!
price: number; // error!
}
is failing to initialize its required properties. If you enable the --strictPropertyInitialization
compiler option which is included in the --strict
suite of compiler features (and you should), this results in compiler errors. Fixing those errors forces you to think about how an instance of the class should actually be constructed, say, by passing constructor arguments for the properties:
class CreateDTO {
id: string;
name: string;
price: number;
constructor(id: string, name: string, price: number) {
this.id = id;
this.name = name;
this.price = price;
}
}
And then you can't say class UpdateDTO extends Partial<CreateDTO>
because Partial
acts only on types, and you need to extend the CreateDTO
class constructor* value. You could write a Partial
function to widen class constructors of a generic type T
to that of Partial<T>
:
function Partial<A extends any[], T extends object>(ctor: new (...args: A) => T) {
const ret: new (...args: A) => Partial<T> = ctor;
return ret;
}
and apply it:
class UpdateDTO extends Partial(CreateDTO) { } // okay
But this doesn't really solve the issue, because your new class still requires those constructor arguments, since it just derives from CreateDTO
:
const u = new UpdateDTO(); // error!
// -----> ~~~~~~~~~~~~~~~
// Expected 3 arguments, but got 0.
Presumably the point of UpdateDTO
is that it doesn't need such arguments. You could keep pushing in this direction, but it feels like the rewards are not worth the cost.
Conceptually, the class UpdateDTO
doesn't really derive from the class CreateDTO
. They're really more like two separate classes which somehow derive from your interface definitions CreateDTO
and Partial<CreateDTO>
.
Therefore it might be more fruitful to think of this as transforming interface definitions into class definitions. Let's make a factory function which takes an object type definition and produces a class constructor that takes a constructor argument of that type and produces an instance of a class also of that type. Like this:
function DTOClass<T extends object>() {
return class {
constructor(arg: any) {
Object.assign(this, arg)
}
} as (new (arg: T) => T);
}
The implementation uses the Object.assign()
method to spread the input argument into the instance being constructed. Then you could make your CreateDTO
and UpdateDTO
classes with it:
interface CreateDTO {
id: string;
name: string;
price: number;
}
const CreateDTO = DTOClass<CreateDTO>();
const c = new CreateDTO({ id: "abc", name: "def", price: 123 });
c.price = 456;
console.log(c); // { "id": "abc", "name": "def", "price": 456 }
}
interface UpdateDTO extends Partial<CreateDTO> { }
const UpdateDTO = DTOClass<UpdateDTO>();
const u = new UpdateDTO({});
u.name = "ghi";
console.log(u); // { "name": "ghi" }
Looks good. The compiler understands that the class constructors produce instances of the relevant object types, and it works as desired at runtime.
The only thing I might change here is that in the case where the object type has no required properties, it would be nice not to require the class constructor to accept an argument. If I'm going to call new UpdateDTO({})
then new UpdateDTO()
should also work. So we can use a conditional type in DTOClass
to make the argument optional if {}
would be an acceptable input:
function DTOClass<T extends object>() {
return class {
constructor(arg: any) {
Object.assign(this, arg)
}
} as {} extends T ? (new (arg?: T) => T) : (new (arg: T) => T);
}
And now you can write
const u = new UpdateDTO();
as well.
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论