Объявить динамически добавляемые свойства класса в TypeScript

Я хочу присвоить свойства экземпляру класса, не зная имен свойств, значений и типов значений в TypeScript. Предположим, у нас есть следующий скрипт 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, даже если он не определен, без ошибок компилятора.

Я не нашел никакой документации, которая проясняет мне это. Может быть, потому что я не носитель английского языка. Поэтому, пожалуйста, отвечайте просто.

Изменить:

Я помню, что было что-то вроде следующего, но я никогда не заставлял это работать:

interface IMyClass {
  [name:string]: any
}

person headacheCoder    schedule 08.12.2016    source источник


Ответы (3)


Проблема в том, что вы добавляете новые свойства во время выполнения, и компилятор не может об этом узнать.

Если вы заранее знаете имена свойств, вы можете сделать это:

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

2-е редактирование

Поскольку вы знаете реквизит, но не в классе, вы можете сделать:

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 и выше.

person Nitzan Tomer    schedule 08.12.2016
comment
Интересный ответ. Но у меня нет возможности узнать свойства заранее. Я думал, что расширение объекта с неизвестными именами свойств во время выполнения не должно быть экзотикой для простого JavaScript, поэтому мне интересно, почему это проблема в TypeScript. - person headacheCoder; 08.12.2016
comment
Таким образом, единственный способ сделать myInstance.foo вместо myInstance['foo'] — это преобразовать myClass в any, верно? - person headacheCoder; 08.12.2016
comment
Если вы используете myInstance.foo, то это означает, что вы заранее знаете, что foo будет в json, поэтому вы можете использовать метод, который я показал в своем первоначальном ответе. Если вы не знаете, что он будет там, то как вы можете иметь его в своем коде? - person Nitzan Tomer; 08.12.2016
comment
Понятно... Спасибо за объяснение. Мой код — это всего лишь абстрактный пример модуля узла, над которым я сейчас работаю, и он должен загружать материал при создании экземпляра. Итак, я знаю, какие свойства есть в классе при импорте и использовании его в другом файле, но сам класс не знает, какие свойства и типы он ему назначит. - person headacheCoder; 08.12.2016
comment
Ответ не решает вопрос. И у Typescript нет хорошего ответа на этот вопрос. Но я проголосовал за, так как лучшей альтернативы, кажется, нет. - person Robert; 05.02.2020

Если вы хотите динамически добавлять свойства класса через объект при создании экземпляра, и информация о типе доступна для этого объекта, вы можете очень хорошо получить полную безопасность типов таким образом (если вы не возражаете против использования статического фабричного метода):

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 параметр для вывода конструктора тип класса. Затем он создает экземпляр и приводит его к объединению типа экземпляра (используя служебный тип InstanceType) и предполагаемого типа свойств, которые вы передали методу.

(Мы могли бы выполнить прямое приведение к Augmentable & U, однако таким образом мы можем расширить класс.)

Примеры

Дополнить основные свойства:

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"

Ограничения

Расширенные свойства на самом деле не являются частью класса, поэтому вы не можете расширить класс, включив в него эти расширения. Это может быть функцией для некоторых случаев использования, когда дополнения являются временными дополнениями, которые не предназначены для изменения иерархии прототипа.

Также непросто добавить нерасширяющие аргументы к .create(), однако простой обходной путь заключается в том, чтобы просто использовать функциональные возможности расширения, чтобы выполнить то же самое, что и с дополнительным аргументом.

person Julien    schedule 08.05.2020

Вы можете добавить в свой класс индексную подпись:

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
  }
}
person vanowm    schedule 17.02.2021