使用打字稿在构造函数中动态设置class 属性

Use typescript to dynamically set class property in constructor

我正在尝试创建一个打字稿 class MyClass 并在构造函数中动态设置实例属性:

const myInstance = new MyClass(({
  someField: 'foo'
}))

myInstance.someField // typescript should show this as type string

如何使用打字稿创建 MyClass 以将 someField 显示为 myInstance 的字符串 属性?

我可以使用泛型吗?有可能吗?

这个怎么样?

class MyClass{
    constructor(props: any) {
        Object.assign(this, props);
    }

    baz(): string {
        return "Hello World";
    }

    static create<T>(props = {} as T) {
        return new MyClass(props || {}) as MyClass & T
    }
}

const o = MyClass.create({
    foo: 1,
    bar: "Hello World"
});

console.log(o.foo);
console.log(o.bar);
console.log(o.baz());


TS playground

从概念上讲,您想说 class MyClass<T extends object> extends Tclass MyClass<T extends object> implements T 之类的内容,但 TypeScript 不允许 class 实例或 interface 具有动态键。 classinterface 声明的所有键必须是静态已知的。因此,如果您想要这种行为,您需要做一些 class 声明以外的事情。

相反,您可以描述所需 MyClass<T> 实例的类型以及 MyClass class 构造函数的类型,然后使用 type assertion 来告诉编译器您的实际构造函数对象属于该类型。

假设您想要的 MyClass<T> class 实例具有 T 的所有属性,外加一个名为 someMethodIGuess() 的方法。那么你的 MyClass<T> 类型可以这样定义:

type MyClass<T extends object> = T & {
    someMethodIGuess(): void;
}

您希望 MyClass class 构造函数具有 construct signature that takes an argument of type T for some generic T 对象类型,并生成 MyClass<T> 的实例。可以这样定义:

type MyClassConstructor = {
    new <T extends object>(arg: T): MyClass<T>
}

要实现这个 class 我们可以使用 [Object.assign()] 将构造函数参数属性复制到实例中,加上我们需要的任何方法或其他东西。

const MyClass = class MyClass {
    constructor(arg: any) {
        Object.assign(this, arg);
    }
    someMethodIGuess() {

    }
} 

当然,如果我们这样保留它,编译器不会将 MyClass 视为 MyClassConstructor(毕竟它不能有动态键,我什至没有试着在这里告诉它 T)。我们需要一个类型断言来告诉它,像这样:

const MyClass = class MyClass {
    constructor(arg: any) {
        Object.assign(this, arg);
    }
    someMethodIGuess() {

    }
} as MyClassConstructor;

编译没有错误。再次注意,编译器无法理解 MyClass 实现符合 MyClassConstructor 类型。通过使用类型断言,我将确保正确实现它的负担从编译器(它不能这样做)转移到我(或者你,如果你使用这段代码)。所以我们应该非常小心地检查我们做了什么并且我们没有写错东西(例如,Object.assign(arg, this); 而不是 Object.assign(this, arg); 将是一个问题)。


我们来测试一下:

const myInstance = new MyClass(({
    someField: 'foo'
}))

console.log(myInstance.someField.toUpperCase()); // FOO
myInstance.someMethodIGuess(); // okay

看起来不错!编译器期望 myInstance 具有 string 类型的 someField 属性 以及 someMethodIGuess() 方法。


就是这样。请注意,使用 MyClass 的方式与使用另一个 class 构造函数的方式相同。例如,编译器永远不会对具有动态键的 class 声明感到满意,因此如果您尝试使用 class 声明创建 MyClass 的通用子 class ,你会得到一个错误:

class SubClass<T extends object> extends MyClass<T> { // error    
    anotherMethod() { }
}

如果你需要那种东西,你会发现自己不得不使用与以前相同的技巧来处理 subclass:

type SubClass<T extends object> = MyClass<T> & { anotherMethod(): void };
type SubClassConstructor = { new <T extends object>(arg: T): SubClass<T> };
const SubClass = class SubClass extends MyClass<any> {
    anotherMethod() { }
} as SubClassConstructor;

Playground link to code