Оформители Typescript отражают метаданные

#typescript #reflect-metadata

#typescript #отражение-метаданных

Вопрос:

Я использую декоратор свойств, Field который передает свой ‘ключ к fields свойству Reflect metadata:

 export function Field(): PropertyDecorator {
    return (target, key) => {
        const fields = Reflect.getMetadata('fields', target) || [];
        if (!fields.includes(key)) {
            fields.push(key)
        }
        Reflect.defineMetadata('fields', fields, target)
    }
}
  

Затем у меня есть абстрактный базовый класс Form , который обращается к метаданным в аксессуаре для получения:

 abstract class Form {
    get fields() {
        return Reflect.getMetadata('fields', this) || [];
    }
}
  

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

 abstract class UserForm extends Form {
    @Field()
    public firstName: string

    @Field()
    public lastName: string

    get fullName() {
        return this.firstName   ' '   this.lastName;
    }
}

class AdminForm extends UserForm {
    @Field()
    roles: string[]
}

const form = new AdminForm()
console.log(form.fields)
// ['roles', 'firstName', 'lastName']
  

Проблема возникает, когда я определяю дочерний класс для AdminForm MemberForm . Когда существует несколько подклассов, Form кажется, что fields средство получения возвращает все поля:

 class MemberForm extends UserForm {
    @Field()
    memberSince: Date;
}

const form = new AdminForm()
console.log(form.fields)
// ['roles', 'firstName', 'lastName', 'memberSince'] <--!!!
  

Для меня это не имеет смысла. Почему memberSince поле появляется в экземпляре AdminForm ? Как я могу определить разные поля в разных подклассах?

Ответ №1:

Проблема в том, что getMetadata происходит по цепочке прототипов и всегда возвращает то, что определено в базовом типе (поскольку это присваивается первым). Вам нужно использовать getOwnMetadata для получения поля массива текущего класса только при добавлении нового поля, а при получении полей вам нужно пройти вверх по цепочке свойств, чтобы получить все поля базового класса.

Это должно сработать:

 import 'reflect-metadata'
export function Field(): PropertyDecorator {
  return (target, key) => {
      const fields = Reflect.getOwnMetadata('fields', target) || [];
      if (!fields.includes(key)) {
          fields.push(key)
      }
      Reflect.defineMetadata('fields', fields, target)
  }
}

abstract class Form {
  get fields() {
      let fields = []
      let target = Object.getPrototypeOf(this);
      while(target != Object.prototype) {
        let childFields = Reflect.getOwnMetadata('fields', target) || [];
        fields.push(...childFields);
        target = Object.getPrototypeOf(target);
      }
      return fields;
  }
}

abstract class UserForm extends Form {
  @Field()
  public firstName!: string

  @Field()
  public lastName!: string

  get fullName() {
      return this.firstName   ' '   this.lastName;
  }
}

class AdminForm extends UserForm {
  @Field()
  roles!: string[]
}

const form1 = new AdminForm()
console.log(form1.fields) // ['roles', 'firstName', 'lastName']

class MemberForm extends UserForm {
  @Field()
  memberSince!: Date;
}

const form2 = new MemberForm()
console.log(form2.fields) // ["memberSince", "firstName", "lastName"]
  

Комментарии:

1. Интересно! Если я изменю getMetadata на getOwnMetadata в декораторе свойств, то я получу ['roles'] включено fields . Итак, что вы подразумеваете под продвижением по цепочке свойств?

2. Вау! Это потрясающе! Большое спасибо!