Spec-Zone.ru › TypeScript 5.1

Миксины

Наряду с традиционными иерархиями ООП, еще один популярный способ построения классов из переиспользуемых компонентов — объединение более простых частичных классов. Вы, возможно, знакомы с концепцией миксинов или трейтов в языках вроде Scala, и этот паттерн также получил популярность в сообществе JavaScript.

Как работают миксины?

Этот паттерн опирается на использование дженериков с наследованием классов для расширения базового класса. Лучшая поддержка миксинов в TypeScript реализуется с помощью паттерна выражения класса. Вы можете узнать больше о работе этого паттерна в JavaScript здесь.

Для начала нам понадобится класс, на который будут накладываться миксины:

class Sprite {
  name = "";
  x = 0;
  y = 0;
 
  constructor(name: string) {
    this.name = name;
  }
}

Затем вам нужен тип и функция-фабрика, которая возвращает выражение класса, расширяющее базовый класс.

// To get started, we need a type which we'll use to extend
// other classes from. The main responsibility is to declare
// that the type being passed in is a class.
 
type Constructor = new (...args: any[]) => {};
 
// This mixin adds a scale property, with getters and setters
// for changing it with an encapsulated private property:
 
function Scale<TBase extends Constructor>(Base: TBase) {
  return class Scaling extends Base {
    // Mixins may not declare private/protected properties
    // however, you can use ES2020 private fields
    _scale = 1;
 
    setScale(scale: number) {
      this._scale = scale;
    }
 
    get scale(): number {
      return this._scale;
    }
  };
}

После настройки вы можете создать класс, представляющий базовый класс с применёнными миксинами:

// Compose a new class from the Sprite class,
// with the Mixin Scale applier:
const EightBitSprite = Scale(Sprite);
 
const flappySprite = new EightBitSprite("Bird");
flappySprite.setScale(0.8);
console.log(flappySprite.scale);

Ограниченные миксины

В вышеприведённом виде миксины не обладают никакими знаниями о классе, что может затруднить создание желаемого дизайна.

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

// This was our previous constructor:
type Constructor = new (...args: any[]) => {};
// Now we use a generic version which can apply a constraint on
// the class which this mixin is applied to
type GConstructor<T = {}> = new (...args: any[]) => T;

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

type Positionable = GConstructor<{ setPos: (x: number, y: number) => void }>;
type Spritable = GConstructor<Sprite>;
type Loggable = GConstructor<{ print: () => void }>;

Затем вы можете создать миксины, которые работают только при наличии определенного базового класса:

function Jumpable<TBase extends Positionable>(Base: TBase) {
  return class Jumpable extends Base {
    jump() {
      // This mixin will only work if it is passed a base
      // class which has setPos defined because of the
      // Positionable constraint.
      this.setPos(0, 20);
    }
  };
}

Альтернативный паттерн

Предыдущие версии этого документа рекомендовали способ написания миксинов, где вы создавали отдельные временные и типовые иерархии, а затем объединяли их в конце:

// Each mixin is a traditional ES class
class Jumpable {
  jump() {}
}
 
class Duckable {
  duck() {}
}
 
// Including the base
class Sprite {
  x = 0;
  y = 0;
}
 
// Then you create an interface which merges
// the expected mixins with the same name as your base
interface Sprite extends Jumpable, Duckable {}
// Apply the mixins into the base class via
// the JS at runtime
applyMixins(Sprite, [Jumpable, Duckable]);
 
let player = new Sprite();
player.jump();
console.log(player.x, player.y);
 
// This can live anywhere in your codebase:
function applyMixins(derivedCtor: any, constructors: any[]) {
  constructors.forEach((baseCtor) => {
    Object.getOwnPropertyNames(baseCtor.prototype).forEach((name) => {
      Object.defineProperty(
        derivedCtor.prototype,
        name,
        Object.getOwnPropertyDescriptor(baseCtor.prototype, name) ||
          Object.create(null)
      );
    });
  });
}

Этот паттерн меньше полагается на компилятор и больше на ваш код, чтобы гарантировать, что временная и система типов корректно синхронизированы.

Ограничения

Паттерн миксинов поддерживается в TypeScript-компиляторе напрямую за счёт анализа потоков кода. Есть несколько случаев, когда вы можете столкнуться с границами этой поддержки.

Декораторы и миксины #4881

Вы не можете использовать декораторы для предоставления миксинов через анализ потоков кода:

// A decorator function which replicates the mixin pattern:
const Pausable = (target: typeof Player) => {
  return class Pausable extends target {
    shouldFreeze = false;
  };
};
 
@Pausable
class Player {
  x = 0;
  y = 0;
}
 
// The Player class does not have the decorator's type merged:
const player = new Player();
player.shouldFreeze;
 
// The runtime aspect could be manually replicated via
// type composition or interface merging.
type FreezablePlayer = Player & { shouldFreeze: boolean };
 
const playerTwo = (new Player() as unknown) as FreezablePlayer;
playerTwo.shouldFreeze;

Статические миксины свойств #17829

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

Вы можете обойти это, используя функции, возвращающие классы, которые отличаются в зависимости от дженерика:

function base<T>() {
  class Base {
    static prop: T;
  }
  return Base;
}
 
function derived<T>() {
  class Derived extends base<T>() {
    static anotherProp: T;
  }
  return Derived;
}
 
class Spec extends derived<string>() {}
 
Spec.prop; // string
Spec.anotherProp; // string

© 2012-2024 Microsoft
Licensed under the Apache License, Version 2.0.
https://www.typescriptlang.org/docs/handbook/mixins.html

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API