【问题标题】:Typescript mixin with protected access具有受保护访问权限的 Typescript mixin
【发布时间】:2022-02-02 06:39:19
【问题描述】:

我有以下混合设置。一个名为 Activatable 的 mixin,需要访问 User 的受保护成员。

type Constructor<T = {}> = new (...args: any[]) => T;

function Activatable<TBase extends Constructor>(Base: TBase) {
  return class extends Base {
    isActivated = false;

    activate() {
      this.isActivated = true;
      this.deactivate();
    }

    deactivate() {
      this.isActivated = false;
    }

    /**
     * In order to access the protected members of User, I needed to
     * add `this` to the args
     */
    test(this: User) {
      this.me = "bo";
      // this method cannot access any other method in the mixin
    }
  };
}

class User {
  name: string;
  protected me: string;

  constructor(name: string) {
    this.name = name;
    this.me = "hi";
  }
}

const ActivatableUser = Activatable(User);

// Instantiate the new `ActivatableUser` class
const user = new ActivatableUser("John Doe");

// Initially, the `isActivated` property is false
console.log(user.isActivated);

// Activate the user
user.activate();
user.test();

// Now, `isActivated` is true
console.log(user.isActivated);

为了让我访问受保护和私有成员(请参阅test()),我需要使用this:User 作为签名的一部分。该方法的缺点是test() 不能再访问Activatable mixin 的其他方法。

我宁愿不必为返回的类表达式命名并使用asthis 转换为mixin 的实例。

既然我知道这个 mixin 将只用于 User 类,有没有办法可以将该信息合并为 Activatable 定义的一部分?所以我不必使用this:User 作为方法签名的一部分?

Playground Link

谢谢大家

【问题讨论】:

  • 喜欢function Activatable&lt;TBase extends new (...args: any[]) =&gt; User&gt;(Base: TBase) {...}?只要您关闭--declaration 编译器选项,这将起作用,因为you can't make easily declarations with mixins right now(另请参阅this one
  • 如果满足您的需求,请告诉我,我会写出来;否则,请在问题中添加更多信息。祝你好运!
  • @jcalz 我认为这会起作用,只要我可以使用 mixin 中的受保护方法。就声明而言,我想这意味着如果我想让一个声明文件被 VSCode 使用,我需要自己编写一个声明文件?谢谢你的帮助。请写下来,我可以接受你的回答。

标签: typescript mixins


【解决方案1】:

在我的项目中,我在使用 mixin 的过程中经历了三个阶段。你的问题正好是我第三阶段的问题,所以我提供我的三阶段解决方案,希望对你有所帮助。

第一阶段:我只需要一个简单的mixin,我不在乎它会被添加到哪个目标类中。对目标类的规范没有要求,所以代码很简单:

type Constructor<T = {}> = new (...args: any[]) => T;

function ActivatableMixin<TBase extends Constructor>(Base: TBase) {
  return class extends Base {
    isActivated = false;
    activate() {
      this.isActivated = true;
      this.deactivate();
    }
    deactivate() {
      this.isActivated = false;
    }
    test() {
      console.log(this.isActivated ? 'activated' : 'not activated');
    }
  };
}

class User {
  name: string;
  me: string;
  constructor(name: string) {
    this.name = name;
    this.me = "hi";
  }
}

const ActivatableUser = ActivatableMixin(User);

// Instantiate the new `ActivatableUser` class
const user = new ActivatableUser("John Doe");

// Initially, the `isActivated` property is false
console.log(user.isActivated);

// Activate the user
user.activate();
user.test();

// Now, `isActivated` is true
console.log(user.isActivated);

第二阶段:我需要能够在mixin中访问目标类的一些成员(属性或方法),所以我也要求目标类必须包含这些成员。如果一个类不包含这些成员,不要指望使用我的 mixin。这需要在第一阶段代码中添加一些规范:

type Constructor<T = {}> = new (...args: any[]) => T;

/**
 * the target class must fit the Activatable Interface to use the mixin
 */
interface Activatable {
  me: string
}

/**
 * Note that the mixin target class specification is specified here
 * <TBase extends Constructor<Activatable>> 
 * instead of
 * <TBase extends Constructor>
 */
function ActivatableMixin<TBase extends Constructor<Activatable>>(Base: TBase) {
  return class extends Base {
    isActivated = false;
    activate() {
      this.isActivated = true;
      this.deactivate();
    }
    deactivate() {
      this.isActivated = false;
    }
    test() {
      // with target class specification we can access members form specification
      console.log(this.me)
      console.log(this.isActivated ? 'activated' : 'not activated');
    }
  };
}

class UserNoMe {
  name: string;
  constructor(name: string) {
    this.name = name;
  }
}

// UserNoMe does not fit Activatable so can not be mixied
const ActivatableUserNoMe = ActivatableMixin(UserNoMe); // ts2345 error

class UserHasMe {
  name: string;
  me: string;
  constructor(name: string) {
    this.name = name;
    this.me = "hi";
  }
}
// UserHasMe fits Activatable so can be mixied
const ActivatableUser = ActivatableMixin(UserHasMe);

// Instantiate the new `ActivatableUser` class
const user = new ActivatableUser("John Doe");

// Initially, the `isActivated` property is false
console.log(user.isActivated);

// Activate the user
user.activate();
user.test();

// Now, `isActivated` is true
console.log(user.isActivated);

第三阶段:我需要在mixin中访问目标类型中的protected成员。这时候,我的第一个想法是在规范界面中将成员改为保护模式。但是接口的成员不能使用protected修饰符(这与接口的职责相违背),所以需要其他方法。最后,我使用以下方法来实现:

type Constructor<T = {}> = new (...args: any[]) => T;

/**
 * The protected members that need to be accessed both in the class 
 * and in the mixin are extracted into this class. This class does not
 * contain any implementation. In fact, it acts as an interface (because 
 * the interface cannot contain protected members). Next, the mixin 
 * specification and the target class should inherit this class.
 * 
 */
class ProtectedMembersCrossClassAndMixin {
  protected me: string;
  protected saySomething() {
    throw new Error('must be implement by ')
  }
}

/**
 * the target class must fit the Activatable Interface to use the mixin
 * with protected menmbers derived from ProtectedMembersCrossClassAndMixin
 */
interface Activatable extends ProtectedMembersCrossClassAndMixin {
  you: string
}

/**
 * Note that the mixin target class specification is specified here
 * <TBase extends Constructor<Activatable>> 
 * instead of
 * <TBase extends Constructor>
 */
function ActivatableMixin<TBase extends Constructor<Activatable>>(Base: TBase) {
  return class extends Base {
    isActivated = false;
    activate() {
      this.isActivated = true;
      this.deactivate();
    }
    deactivate() {
      this.isActivated = false;
    }
    test() {
      // with target class specification we can access members within specification
      console.log(this.you)
      // alse access protected members
      console.log(this.me);
      console.log(this.isActivated ? 'activated' : 'not activated');
    }
    /**
     * also access or override protected method members
     */  
    saySomething() {
      super.saySomething();
      console.log('hello activatable mixed');
    }
  };
}

class UserNo {
  name: string;
  you: string;
  protected me: string;
  constructor(name: string) {
    this.name = name;
  }
  protected saySomething() {
    console.log('oh my');
  }
}

// UserNo does fit Activatable 
// but not derive from  ProtectedMembersCrossClassAndMixin 
// so can not be mixied
const ActivatableUserNoMe = ActivatableMixin(UserNo); // ts2345 error

// this one can be mixed
class UserHas extends ProtectedMembersCrossClassAndMixin {
  name: string;
  you: string;
  protected me: string;
  constructor(name: string) {
    super();
    this.name = name;
    this.me = "hi";
  }
  /**
   * typescript will not warn you if you forget to implement this method
   * because typescript thought it's implemented in  ProtectedMembersCrossClassAndMixin
   * which is not really
   */ 
  protected saySomething() {
    console.log('hello');
  }
}
// UserHas fits Activatable so can be mixied
const ActivatableUser = ActivatableMixin(UserHas);

// Instantiate the new `ActivatableUser` class
const user = new ActivatableUser("John Doe");

// Initially, the `isActivated` property is false
console.log(user.isActivated);

// Activate the user
user.activate();
user.test();

// Now, `isActivated` is true
console.log(user.isActivated);

ProtectedMembersCrossClassAndMixin 的注意事项:

  1. 类的代码与接口的代码不同。编译后它仍然存在,所以我们应该保持类最小化,所以不要包含任何实现代码。
  2. 只有真正需要保护的才放到这个类里,其他的还是放到接口里
  3. 还有另一个层次的继承。
  4. 别忘了:基类和mixin规范接口的两端都应该扩展这个类
  5. 从此类继承的子类可能会忘记实现那些继承的成员
  6. 当你真正知道你需要它并了解它的缺陷时使用它

【讨论】:

    【解决方案2】:

    在 Github 上长期研究和测试大量 typescript mixins/traits 库后,我偶然发现了这个库,它真的很棒。

    https://github.com/michaelolof/typescript-mix

    事实证明它解决了这些解决方案中报告的所有问题https://stackoverflow.com/a/65418734/8552565

    它允许访问基类的私有和受保护属性,这太酷了。

    ...它使用装饰器和类型合并的方式非常简洁。

    【讨论】:

      猜你喜欢
      • 2021-11-28
      • 2014-05-11
      • 2013-04-09
      • 2016-09-09
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多