Mixin作为TypeScript中的类装饰器不会更新类属性

时间:2018-06-22 10:54:44

标签: typescript decorator mixins

简介

我在TS中有一个项目,该项目需要一些类来实现以下接口:

interface IStylable {
  readonly styles: {
    [property: string]: string
  };
  addStyles (styles: { [property: string]: string }): void;
  updateStyles (styles: { [property: string]: string }): void;
  removeStyles (styles: Array<string>): void;
}

为了避免样板代码,我决定创建一个Mixin并将其应用于我需要的每个类中。 (我可以使用抽象类,但是我的问题需要多重继承解决方案,而TS并未提供这种解决方案。)以下是IStylable接口的类实现:

export class StylableClass implements IStylable {
  private readonly _styles: { [property: string]: string } = {};

  // For each property provided in styles param, check if the property
  // is not already present in this._styles and add it. This way we
  // do not overide existing property values.
  public addStyles (styles: { [property: string]: string }): void {
    for (const [property, value] of Object.entries(styles)) {
      if (!this._styles.hasOwnProperty(property)) {
        this._styles[property] = value;
      }
    }
  }

  // For each property provided in styles param, check if the property
  // is already present in this._styles and add it. This way we
  // do add property values values that do not exist.
  public updateStyles (styles: { [property: string]: string }): void {
    for (const [property, value] of Object.entries(styles)) {
      if (this._styles.hasOwnProperty(property)) {
        this._styles[property] = value;
      }
    }
  }

  // For each property in styles param, check if it is present in this._styles
  // and remove it.
  public removeStyles (styles: Array<string>): void {
    for (const property of styles) {
      if (this._styles.hasOwnProperty(property)) {
        delete this._styles[property];
      }
    }
  }

  public set styles (styles: { [property: string]: string }) {
    this.addStyles(styles);
  }

  public get styles (): { [property: string]: string } {
    return this._styles;
  }
}

对于我真的很兴奋的事情,期待着ES6中装饰器规范的标准化。打字稿通过在experimentalDecorators中设置tsconfig.json标志来允许此实验功能。我希望将StylableClass用作类装饰器(@Stylable)以使代码更整洁,因此我创建了一个函数,该函数接受一个类并将其转换为装饰器:

export function makeDecorator (decorator: Function) {
  return function (decorated: Function) {
    const fieldCollector: { [key: string]: string } = {};
    decorator.apply(fieldCollector);
    Object.getOwnPropertyNames(fieldCollector).forEach((name) => {
      decorated.prototype[name] = fieldCollector[name];
    });

    Object.getOwnPropertyNames(decorator.prototype).forEach((name) => {
      decorated.prototype[name] = decorator.prototype[name];
    });
  };
}

并按如下方式使用它:

export const Stylable = () => makeDecorator(StylableClass);

问题

现在该进行单元测试了。我创建了一个虚拟类来应用我的装饰器,并为addStyles()方法编写了一个简单的测试。

@Stylable()
class StylableTest {
  // Stylable
  public addStyles!: (styles: {
    [prop: string]: string;
  }) => void;

  public updateStyles!: (styles: {
    [prop: string]: string;
  }) => void;

  public removeStyles!: (styles: string[]) => void;

  public styles: { [property: string]: string } = {};
}

describe('Test Stylable mixin', () => {
  it('should add styles', () => {
    const styles1 = {
      float: 'left',
      color: '#000'
    };

    const styles2 = {
      background: '#fff',
      width: '100px'
    };

    // 1
    const styles = new StylableTest();
    expect(styles.styles).to.be.an('object').that.is.empty;

    // 2
    styles.addStyles(styles1);
    expect(styles.styles).to.eql(styles1);

    // 3
    styles.addStyles(styles2);
    expect(styles.styles).to.eql(Object.assign({}, styles1, styles2));
  });
});

问题是第二个Expect语句失败。执行styles.addStyles(styles1);后,styles.styles数组应包含styles1对象时仍为空。在调试代码时,我发现push方法中的addStyles()语句已按预期执行,因此循环没有问题,但是在方法执行结束后不会更新数组。您能给我提示或解决我错过的事情吗?我检查的第一件事是makeDecorator函数可能出了问题,但是只要我能执行方法,就找不到其他要寻找的线索。

1 个答案:

答案 0 :(得分:1)

StylableClass混合声明了一个名为styles的属性。但是StylableTest创建一个字段名styles并为其分配一个空对象,任何人都不会使用。您需要将属性描述从装饰器转移到目标类,并从= {}中的styles中删除StylableTest

function makeDecorator(decorator) {
    return function (decorated) {
        var fieldCollector = {};
        decorator.apply(fieldCollector);
        Object.getOwnPropertyNames(fieldCollector).forEach(function (name) {
            decorated.prototype[name] = fieldCollector[name];
        });
        Object.getOwnPropertyNames(decorator.prototype).forEach(function (name) {
            var descriptor = Object.getOwnPropertyDescriptor(decorator.prototype, name);
            if (descriptor) {
                Object.defineProperty(decorated.prototype, name, descriptor);
            }
            else {
                decorated.prototype[name] = decorator.prototype[name];
            }
        });
    };
}

我可以建议使用less error prone approach来混合打字稿中的mixins。这种必须重新声明所有mixin成员的方法以后会导致错误。至少避免使用类型查询来重述字段的类型:

@Stylable()
class StylableTest {
    // Stylable
    public addStyles!: IStylable['addStyles']

    public updateStyles!: IStylable['updateStyles']

    public removeStyles!: IStylable['removeStyles']

    public styles!: IStylable['styles']
}