簡體   English   中英

我需要如何更改這些 TypeScript mixin 類型定義,以便允許定義允許 class 擴展特征的 mixin?

[英]How do I need to change these TypeScript mixin type definitions in order to allow the definition of mixins that allow a class to extend the trait?

出於此問題的目的,將“mixin”視為 function,如https://www.typescriptlang.org/docs/handbook/mixins.html 所述 在這種情況下,mixin 擴展了接收 mixin 的 class。 我正在嘗試做一些不同的事情:啟用“特征”,我在這里將其定義為可重用的類,這些類提供公共和非公共實例成員,可以由擴展特征的 class 繼承和覆蓋,這與 mixin 不同.

隨后嘗試了解決方案,但打字不太正確,這就是我堅持的部分。 請注意,這在 JavaScript 中完美運行,正如我編寫的 npm package @northscaler/mutrait所證明的那樣。

我的問題是如何更改下面的類型定義以使代碼能夠編譯並通過測試?

首先,這是模塊traitify.ts ,它試圖成為這個的“庫”(我知道它的類型定義不正確):

// in file traitify.ts

/**
 * Type definition of a constructor.
 */
export type Constructor<T> = new(...args: any[]) => T;

/**
 * A "trait" is a function that takes a superclass `S` and returns a new class `T extends S`.
 */
export type Trait<S extends Constructor<object>, T extends S> = (superclass: S) => T

/**
 * Convenient function when defining a class that
 * * extends a superclass, and
 * * expresses one or more traits.
 */
export const superclass = <S extends Constructor<object>>(s?: S) => new TraitBuilder(s)

/**
 * Convenient function to be used when a class
 * * does not extend a superclass, and
 * * expresses multiple traits.
 */
export const traits = <S extends Constructor<object>, T extends S>(t: Trait<S, T>) => superclass().with(t)

/**
 * Convenient function to be used when defining a class that
 * * does not extend a superclass, and
 * * expresses exactly one trait.
 */
export const trait = <S extends Constructor<object>, T extends S>(t: Trait<S, T>) => traits(t).apply()

/**
 * A convenient trait applier class that uses a builder pattern to apply traits.
 */
class TraitBuilder<S extends Constructor<object>> {
  superclass: S;

  constructor (superclass?: S) {
    this.superclass = superclass || class {} as S // TODO: remove "as S" when figured out
  }

  /**
   * Applies the trait to the current superclass then returns a new `TraitBuilder`.
   * @param trait The trait that the current superclass should express.
   */
  with <S extends Constructor<object>, T extends S>(trait: Trait<S, T>) {
    // we have to return a new builder here because there's no way to take a collection of traits of differing types.
    return new TraitBuilder(trait(this.superclass))
  }

  /**
   * Return the class with all traits expressed.
   */
  apply() {
    return this.superclass || class {}
  }
}

我希望能夠在Taggable.ts中定義一個Taggable特征,如下所示,其中該特征定義了一個受保護的_tag字段,並提供了tag屬性的默認實現:

// in file Taggable.ts

import { Constructor } from './traitify';

export interface ITaggable {
  tag?: string;
}

export const Taggable = <S extends Constructor<object>>(superclass: S) =>
  class extends superclass implements ITaggable {
    _tag?: string; // TODO: make protected when https://github.com/microsoft/TypeScript/issues/36060 is fixed

    get tag() {
      return this._tag;
    }

    set tag(tag) {
      this._doSetTag(this._testSetTag(tag));
    }

    constructor(...args: any[]) {
      super(...args);
    }

    _testSetTag(tag?: string) { // TODO: make protected
      return tag;
    }

    _doSetTag(tag?: string) { // TODO: make protected
      this._tag = tag;
    }
  };

tag屬性的默認實現是有意為之的,因為在這種模式中,我希望允許extend特征的類僅覆蓋它希望覆蓋的那些特征成員。

在保持示例最小但完整的同時,我必須包含一個更多的示例特征來說明當 class extend多個特征時的模式,因此這是一個可命名的特征,與TaggableNameable非常相似。

// in file Nameable.ts

import { Constructor } from './traitify';

export interface INameable {
  name?: string;
}

export const Nameable = <S extends Constructor<object>>(superclass: S) =>
  class extends superclass implements INameable {
    _name?: string; // TODO: make protected when https://github.com/microsoft/TypeScript/issues/36060 is fixed

    get name() {
      return this._name;
    }

    set name(name) {
      this._doSetName(this._testSetName(name));
    }

    constructor(...args: any[]) {
      super(...args);
    }

    _testSetName(name?: string) { // TODO: make protected
      return name;
    }

    _doSetName(name?: string) { // TODO: make protected
      this._name = name;
    }
  };

現在,有了我們的traitify庫和兩個特征,這里是我試圖通過的測試,它們說明了特征的消費者將如何使用它:

import { trait, superclass } from './traitify';

import test from 'ava';
import { Taggable } from './Taggable';
import { Nameable } from './Nameable';

test('express a single trait with no superclass', (t) => {
  class Point extends trait(Taggable) {
    constructor(public x: number, public y: number) {
      super(...arguments);
      this.x = x;
      this.y = y;
    }

    _testSetTag(tag?: string) {
      tag = super._testSetTag(tag);

      if (!tag) throw new Error('no tag given');
      else return tag.toLowerCase();
    }
  }

  const point = new Point(10, 20);
  point.tag = 'hello';

  t.is(point.tag, 'hello');
  t.throws(() => point.tag = '');
});

test('express a single trait and extend a superclass', (t) => {
  class Base {
    something: string = 'I am a base';
  }

  class Sub extends superclass(Base)
    .with(Taggable).apply() {

    constructor() {
      super(...arguments);
    }

    _testSetTag(tag?: string): string | undefined {
      tag = super._testSetTag(tag);

      if (tag === 'throw') throw new Error('illegal tag value');
      return tag;
    }
  }

  const sub = new Sub();

  t.assert(sub instanceof Sub);
  t.assert(sub instanceof Base);

  sub.tag = 'sub';

  t.is(sub.tag, 'sub');
  t.throws(() => sub.tag = 'throw');
});

test('express multiple traits and extend a superclass', (t) => {
  class Animal {
  }

  class Person extends superclass(Animal)
    .with(Nameable)
    .with(Taggable).apply() {

    constructor(...args: any[]) {
      super(args);
    }

    _testSetName(name?: string) {
      if (!name) throw new Error('no name given');
      return name.trim();
    }
  }

  const person = new Person();

  t.assert(person instanceof Person);
  t.assert(person instanceof Animal);

  person.name = 'Felix';

  t.is(person.name, 'Felix');
  t.throws(() => person.name = null);
});

test('superclass expresses a trait, subclass expresses another trait but overrides method in superclass\'s trait', (t) => {
  class Animal extends trait(Nameable) {
    constructor(...args: any[]) {
      super(args);
    }

    _testSetName(name?: string) {
      if (!name) throw new Error('no name given');
      if (name.toLowerCase().includes('animal')) throw new Error('name must include "animal"');
      return name;
    }
  }

  const animal = new Animal();
  animal.name = 'an animal';

  t.is(animal.name, 'an animal');
  t.throws(() => animal.name = 'nothing');

  class Person extends superclass(Animal)
    .with(Taggable).apply() {

    constructor(...args: any[]) {
      super(args);
    }

    _testSetName(name?: string) {
      if (!name) throw new Error('no name given');
      if (name.toLowerCase().includes('person')) throw new Error('name must include "person"');
      return name;
    }
  }

  const person = new Person();
  t.assert(person instanceof Person);
  t.assert(person instanceof Animal);

  person.name = 'a person';

  t.is(person.name, 'a person');
  t.throws(() => person.name = 'an animal');
  t.throws(() => person.name = 'nothing');
});

我得到的編譯器錯誤如下:

src/lib/traitify.spec.ts:84:10 - error TS2339: Property 'name' does not exist on type 'Person'.

84   person.name = 'Felix';
            ~~~~

src/lib/traitify.spec.ts:86:15 - error TS2339: Property 'name' does not exist on type 'Person'.

86   t.is(person.name, 'Felix');
                 ~~~~

src/lib/traitify.spec.ts:87:25 - error TS2339: Property 'name' does not exist on type 'Person'.

87   t.throws(() => person.name = null);
                           ~~~~

src/lib/traitify.spec.ts:127:10 - error TS2339: Property 'name' does not exist on type 'Person'.

127   person.name = 'a person';
             ~~~~

src/lib/traitify.spec.ts:129:15 - error TS2339: Property 'name' does not exist on type 'Person'.

129   t.is(person.name, 'a person');
                  ~~~~

src/lib/traitify.spec.ts:130:25 - error TS2339: Property 'name' does not exist on type 'Person'.

130   t.throws(() => person.name = 'an animal');
                            ~~~~

src/lib/traitify.spec.ts:131:25 - error TS2339: Property 'name' does not exist on type 'Person'.

131   t.throws(() => person.name = 'nothing');
                            ~~~~

src/lib/traitify.ts:48:35 - error TS2345: Argument of type 'S' is not assignable to parameter of type 'S'.
  'S' is assignable to the constraint of type 'S', but 'S' could be instantiated with a different subtype of constraint 'Constructor<object>'.
    Type 'Constructor<object>' is not assignable to type 'S'.
      'Constructor<object>' is assignable to the constraint of type 'S', but 'S' could be instantiated with a different subtype of constraint 'Constructor<object>'.

48     return new TraitBuilder(trait(this.superclass))
                                     ~~~~~~~~~~~~~~~


Found 8 errors.

注意:如果您想在https://github.com/matthewadams/typescript-trait-test 上使用它,可以使用 Git 存儲庫。 要播放,請執行git clone https://github.com/matthewadams/typescript-trait-test && cd typescript-trait-test && npm install && npm test

注意:我覺得這真的是我所能提供的最小限度的演示我正在嘗試啟用的模式。

好吧,我終於在TypeScript中找出了一個不完美但可用的特征模式。

TL;DR:如果您只想查看演示該模式的代碼,請點擊此處 查看main文件夾和test文件夾。

出於本次討論的目的, trait只是一個 function ,它接受一個可選的超類並返回一個新的 class ,它擴展給定的超類實現一個或多個特定於特征的接口。 這是這里看到的著名子類工廠模式和交叉類型的變體的組合。

這是強制性的,盡管太小了,“你好,世界。”。

這是圖書館,如果你想這樣稱呼它:

/**
 * Type definition of a constructor.
 */
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export type Constructor<T> = new (...args: any[]) => T

/**
 * The empty class.
 */
export class Empty {}

/**
 * A "trait" is a function that takes a superclass of type `Superclass` and returns a new class that is of type `Superclass & TraitInterface`.
 */
// eslint-disable-next-line @typescript-eslint/ban-types
export type Trait<Superclass extends Constructor<object>, TraitInterface> = (
  superclass: Superclass
) => Constructor<Superclass & TraitInterface>

這是一個Greetable特征,它賦予一個greeting屬性和一種問候某人的方法。 請注意,它包含一個用於返回的特征implements的公共行為的接口。

// in file Greetable.ts

import { Constructor, Empty } from '../main/traitify'

/*
 * Absolutely minimal demonstration of the trait pattern, in the spirit of "Hello, world!" demos.
 * This is missing some common stuff because it's so minimal.
 * See Greetable2 for a more realistic example.
 */

/**
 * Public trait interface
 */
export interface Public {
  greeting?: string

  greet(greetee: string): string
}

/**
 * The trait function.
 */
// eslint-disable-next-line @typescript-eslint/ban-types, @typescript-eslint/explicit-module-boundary-types
export const trait = <S extends Constructor<object>>(superclass?: S) =>
  /**
   * Class that implements the trait
   */
  class Greetable extends (superclass || Empty) implements Public {
    greeting?: string

    /**
     * Constructor that simply delegates to the super's constructor
     */
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    constructor(...args: any[]) {
      super(...args)
    }

    greet(greetee: string): string {
      return `${this.greeting}, ${greetee}!`
    }
  }

以下是如何編寫 class 來表達該特征。 這是來自我的摩卡單元測試。

// in file traitify.spec.ts

  it('expresses the simplest possible "Hello, world!" trait', function () {
    class HelloWorld extends Greetable.trait() {
      constructor(greeting = 'Hello') {
        super()
        this.greeting = greeting
      }
    }

    const greeter = new HelloWorld()

    expect(greeter.greet('world')).to.equal('Hello, world!')
  })

現在您已經看到了最簡單的示例,讓我分享一個更現實的“Hello,world”。 這演示了一些更有用的東西,這是另一個 Greetable 特性,但它的行為可以通過表達 class 來定制

// in file Greetable2.ts

import { Constructor, Empty } from '../main/traitify'

/**
 * Public trait interface
 */
export interface Public {
  greeting?: string

  greet(greetee: string): string
}

/**
 * Nonpublic trait interface
 */
export interface Implementation {
  _greeting?: string

  /**
   * Validates, scrubs & returns given value
   */
  _testSetGreeting(value?: string): string | undefined

  /**
   * Actually sets given value
   */
  _doSetGreeting(value?: string): void
}

/**
 * The trait function.
 */
// eslint-disable-next-line @typescript-eslint/ban-types, @typescript-eslint/explicit-module-boundary-types
export const trait = <S extends Constructor<object>>(superclass?: S) =>
  /**
   * Class that implements the trait
   */
  class Greetable2 extends (superclass || Empty) implements Implementation {
    _greeting?: string

    /**
     * Constructor that simply delegates to the super's constructor
     */
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    constructor(...args: any[]) {
      super(...args)
    }

    get greeting() {
      return this._greeting
    }

    set greeting(value: string | undefined) {
      this._doSetGreeting(this._testSetGreeting(value))
    }

    greet(greetee: string): string {
      return `${this.greeting}, ${greetee}!`
    }

    _testSetGreeting(value?: string) {
      return value
    }

    _doSetGreeting(value?: string) {
      this._greeting = value
    }
  }

這具有包括兩個接口的關鍵特征,一個用於公共行為,一個用於非公共行為。 Public接口表示對表達特征的類的客戶端可見的行為。 Implementation接口將實現的細節表示為一個接口,並且Implementation extends Public 然后特征 function 返回一個implements Implementation的 class。

在此實現中, _testSetGreeting方法驗證、清理並返回所設置的值,而_doSetGreeting實際上設置支持屬性_greeting

現在,表示特征的 class 可以覆蓋它需要的任何內容以自定義行為。 此示例覆蓋_testSetGreeting以確保提供問候語並修剪問候語。

// in file traitify.spec.ts

  it('expresses a more realistic "Hello, world!" trait', function () {
    class HelloWorld2 extends Greetable2.trait() {
      constructor(greeting = 'Hello') {
        super()
        this.greeting = greeting
      }

      /**
       * Overrides default behavior
       */
      _testSetGreeting(value?: string): string | undefined {
        value = super._testSetGreeting(value)

        if (!value) {
          throw new Error('no greeting given')
        }

        return value.trim()
      }
    }

    const greeter = new HelloWorld2()

    expect(greeter.greet('world')).to.equal('Hello, world!')
    expect(() => {
      greeter.greeting = ''
    }).to.throw()
  })

repo中有更詳盡的示例。

有時,TypeScript 仍然會出錯,通常是當您表達不止一種特征時,這是常見的。 有一個方便的方法,呃,幫助TypeScript 獲得正確類型的表達特征的類:將表達類的構造函數的 scope 減少為protected並創建一個名為new的 static 工廠方法,它返回表達 class 的實例,使用as告訴 88349418774638正確的類型是什么。 這是一個例子。

  it('express multiple traits with no superclass', function () {
    class Point2 extends Nameable.trait(Taggable.trait()) {
      // required to overcome TypeScript compiler bug?
      static new(x: number, y: number) {
        return new this(x, y) as Point2 & Taggable.Public & Nameable.Public
      }

      protected constructor(public x: number, public y: number) {
        super(x, y)
      }

      _testSetTag(tag?: string) {
        // eslint-disable-next-line @typescript-eslint/ban-ts-comment
        // @ts-ignore
        tag = super._testSetTag(tag)

        if (!tag) throw new Error('no tag given')
        else return tag.toLowerCase()
      }

      _testSetName(name?: string) {
        name = super._testSetName(name)

        if (!name) throw new Error('no name given')
        else return name.toLowerCase()
      }
    }

    const point2 = Point2.new(10, 20)
    point2.tag = 'hello'

    expect(point2.tag).to.equal('hello')
    expect(() => (point2.tag = '')).to.throw()
  })

付出的代價相當小:您使用類似const it = It.new()的東西而不是const it = new It() 這會讓你走得更遠,但你仍然需要在這里和那里撒一個// @ts-ignore來讓 TypeScript 知道知道你在做什么。

最后,有一個限制。 如果您希望 class 表達特征擴展基類 class,如果它們具有相同的名稱,特征將覆蓋基類的方法。 然而在實踐中,這並不是一個嚴重的限制,因為一旦你采用了特征模式,它就有效地取代了傳統的extends用法。

您應該始終任何可重用的基於類的代碼編寫為特征,然后讓您的類表達特征,而不是擴展基類。

概括

我知道這並不完美,但效果很好。 願意看到 TypeScript 提供trait / mixin & with關鍵字,這些關鍵字是此模式的語法糖,很像Dart 的mixin & withScala 的 traits ,它與此模式類似,除了所有 scope & 類型安全問題都會得到解決(以及鑽石問題的解決方案)。

注意:已經有一個JavaScript 的 mixins 提案

這個模式我花了很長時間來識別,它仍然不對,但現在已經足夠好了。 這種特征模式在 JavaScript 中對我很有用,但我在嘗試在 TypeScript 中表現出相同的模式時總是遇到問題(而且我不是唯一的)。

現在,還有其他解決方案試圖解決這個問題,但我在這里提出的方案在簡單性、可讀性、可理解性和功能之間取得了很好的平衡。 讓我知道你的想法。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM