如何使Typescript中的所有类属性可选

huangapple go评论51阅读模式
英文:

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&lt;CreateDTO&gt; {}

<!-- 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&lt;CreateDTO&gt; {}

<!-- 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派生出来的。它们更像是从您的接口定义CreateDTOPartial<CreateDTO>派生出来的两个单独的类。因此,将这看作将接口定义转换为类定义可能更有意义。可以创建一个工厂函数,该函数接受一个对象类型定义并生成一个类构造函数,该构造函数接受该类型的构造函数参数并生成该类型的类实例。实现使用Object.assign()方法将输入参数传播到正在构建的实例中。然后,可以使用它来创建CreateDTOUpdateDTO类。唯一可能更改的是,在对象类型没有必需属性的情况下,如果{}是可接受的输入,不需要强制类构造函数接受参数。可以在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&lt;CreateDTO&gt; 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&lt;T&gt;:

function Partial&lt;A extends any[], T extends object&gt;(ctor: new (...args: A) =&gt; T) {        
    const ret: new (...args: A) =&gt; Partial&lt;T&gt; = 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!
// -----&gt; ~~~~~~~~~~~~~~~
// 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&lt;CreateDTO&gt;.


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&lt;T extends object&gt;() {
    return class {
        constructor(arg: any) {
            Object.assign(this, arg)
        }
    } as (new (arg: T) =&gt; 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&lt;CreateDTO&gt;();

const c = new CreateDTO({ id: &quot;abc&quot;, name: &quot;def&quot;, price: 123 });
c.price = 456;
console.log(c); // { &quot;id&quot;: &quot;abc&quot;, &quot;name&quot;: &quot;def&quot;, &quot;price&quot;: 456 }

}

interface UpdateDTO extends Partial&lt;CreateDTO&gt; { }
const UpdateDTO = DTOClass&lt;UpdateDTO&gt;();
const u = new UpdateDTO({});
u.name = &quot;ghi&quot;;
console.log(u); // { &quot;name&quot;: &quot;ghi&quot; } 

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&lt;T extends object&gt;() {
    return class {
        constructor(arg: any) {
            Object.assign(this, arg)
        }
    } as {} extends T ? (new (arg?: T) =&gt; T) : (new (arg: T) =&gt; T);
}

And now you can write

const u = new UpdateDTO();

as well.

Playground link to code

huangapple
  • 本文由 发表于 2023年4月4日 06:58:59
  • 转载请务必保留本文链接:https://go.coder-hub.com/75924312.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定