在 TypeScript 中声明动态添加的 class 属性

Declare dynamically added class properties in TypeScript

我想在不知道 TypeScript 中的 属性 名称、值和值类型的情况下将属性分配给 class 的实例。假设我们有以下 example.ts 脚本:

// This could be a server response and could look totally diffent another time...
const someJson:string = '{ "foo": "bar", "bar": "baz" }'

class MyClass {
  someProperty:boolean

  constructor( json:string ) {
    const parsedJson:any = JSON.parse( json )

    Object.keys( parsedJson ).forEach(
      ( key:string ) => {
        this[ key ] = parsedJson[ key ]
      }
    )

    this['someProperty'] = true
  }
}

const myInstance = new MyClass( someJson )

// Works fine, logs `true`.
console.log( myInstance.someProperty )

// Error: Property 'foo' does not exist on type 'MyClass'.
console.log( myInstance.foo )

// Error: Property 'bar' does not exist on type 'MyClass'.
console.log( myInstance.bar )

如何确保 TypeScript 编译器不会抱怨动态添加的属性,而是将它们作为任何类型的 "key": value 对来处理。我仍然希望 tsc 确保 myInstance.someProperty 必须是 boolean 类型,但我希望能够获得 myInstance.whatever 即使它没有定义 [=33] =] 进入编译器错误。

我没有找到任何说明这一点的文档。也许是因为我不是以英语为母语的人。所以请保持简单的答案。

编辑:

我记得有类似下面的东西,但我从来没有让它起作用:

interface IMyClass {
  [name:string]: any
}

问题是您在运行时添加了新属性,而编译器无法知道这一点。

如果您事先知道 属性 个名字,那么您可以这样做:

type Json = {
    foo: string;
    bar: string;
}

...

const myInstance = new MyClass(someJson) as MyClass & Json;
console.log(myInstance.foo) // no error

编辑

如果您事先不知道这些属性,那么您不能这样做:

console.log(myInstance.foo);

因为你知道 foo 是收到的 json 的一部分,你可能会得到类似的东西:

let key = getKeySomehow();
console.log(myInstance[key]);

这应该可以在编译器没有错误的情况下工作,唯一的问题是编译器不知道返回值的类型,它将是 any.

所以你可以这样做:

const myInstance = new MyClass(someJson) as MyClass & { [key: string]: string };
let foo = myInstance["foo"]; // type of foo is string
let someProperty = myInstance["someProperty"]; // type of someProperty is boolean

第二次编辑

如你知道的道具,但不在class,你可以这样做:

type ExtendedProperties<T> = { [P in keyof T]: T[P] };
function MyClassFactory<T>(json: string): MyClass & ExtendedProperties<T> {
    return new MyClass(json) as MyClass & ExtendedProperties<T>;
}

然后你就可以像这样使用它:

type Json = {
    foo: string;
    bar: string;
};
const myInstance = MyClassFactory<Json>(someJson);

请注意,这仅适用于 typescript 2.1 及更高版本。

如果你想在实例化时通过对象动态添加 class 属性,并且类型信息可用于该对象,你可以通过这种方式很好地获得完整的类型安全(只要你不不介意使用静态工厂方法):

class Augmentable {
 constructor(augment: any = {}) {
   Object.assign(this, augment)
 }
 static create<T extends typeof Augmentable, U>(this: T, augment?: U) {
   return new this(augment) as InstanceType<T> & U
 }
}

这是使用(假)this parameter 来推断 class 的构造函数类型。然后它构造实例,并将其转换为实例类型(使用 InstanceType 实用程序类型)和您传递给方法的道具的推断类型的联合。

(我们可以直接转换为 Augmentable & U,但是这种方式允许我们扩展 class。)

例子

增强基本属性:

const hasIdProp = Augmentable.create({ id: 123 })
hasIdProp.id // number

增强方法:

const withAddedMethod = Augmentable.create({
  sayHello() {
    return 'Hello World!'
  }
})


withAddedMethod.sayHello() // Properly typed, with signature and return value

扩展和扩充,在方法扩充中使用 this 访问:

class Bob extends Augmentable {
  name = 'Bob'
  override = 'Set from class definition'
  checkOverrideFromDefinition() {
    return this.override
  }
}

interface BobAugment {
  whatToSay: string
  override: string
  sayHelloTo(to: string): void
  checkOverrideFromAugment(): string
}

const bobAugment: BobAugment = {
  whatToSay: 'hello',
  override: 'Set from augment'

  sayHelloTo(this: Bob & BobAugment, to: string) {
    // Let's combine a class parameter, augment parameter, and a function parameter!
    return `${this.name} says '${this.whatToSay}' to ${to}!`
  },

  checkOverrideFromAugment(this: Bob & BobAugment) {
    return this.override
  }
}

const bob = Bob.create(bobAugment) // Typed as Bob & BobAugment
bob.sayHelloTo('Alice') // "Bob says 'hello' to Alice!"

// Since extended class constructors always run after parent constructors,
// you cannot override a class-set parameter with an augment, no matter
// from where you are checking it.
bob.checkOverrideFromAugment() // "Set from class definition"
bob.checkOverrideFromDefinition() // "Set from class definition"

限制

增强的属性实际上并不是 class 的一部分,因此您不能扩展包含这些增强的 class。这可能是某些用例的一个特性,其中扩充是临时添加,并不意味着修改原型层次结构

.create() 添加非扩充参数也不容易,但是一个简单的解决方法是简单地利用扩充功能来完成与额外参数相同的事情。

您可以将 index signature 添加到您的 class:

class MyClass {
  [index: string]: any; //index signature

  someProperty:boolean

  constructor( json:string ) {
    const parsedJson:any = JSON.parse( json )

    Object.keys( parsedJson ).forEach(
      ( key:string ) => {
        this[ key ] = parsedJson[ key ]
      }
    )

    this['someProperty'] = true
  }
}