TypeScript 5.0 introduces us to ECMAScript decorators, which are quite different from the experimental decorators of the past. With a little bit of wiring, and some very specific constraints, we can build a new kind of mix-in class.
MultiMixinBuilder and its helper types (SubclassDecorator, StaticAndInstance most notably) provide everything you need to build out your mix-in. The main benefit of MultiMixinBuilder is it returns a Class with an aggregate type, combining all the static and instance fields you defined. Built-in TypeScript 5 decorators don't provide the aggregate type.
import MultiMixinBuilder, {
type StaticAndInstance,
type SubclassDecorator,
} from "mixin-decorators";
class MixinBase {
// eslint-disable-next-line @typescript-eslint/no-unused-vars, @typescript-eslint/no-explicit-any
constructor(...args: any[])
{
// do nothing
}
}
// #region XVector
declare const XVectorKey: unique symbol;
interface XVector extends StaticAndInstance<typeof XVectorKey> {
staticFields: {
xCoord: number;
}
instanceFields: {
get xLength(): number;
set xLength(value: number);
}
symbolKey: typeof XVectorKey;
}
const Mixin_XVector: SubclassDecorator<XVector, typeof MixinBase, false> = function(
this: void,
_class: typeof MixinBase,
context: ClassDecoratorContext<typeof MixinBase>,
)
{
if (context.kind !== "class") {
throw new Error("what's happening?")
}
return class extends _class {
static xCoord = 12;
xLength = 0;
constructor(...args: unknown[]) {
super(...args);
}
}
}
// #endregion XVector
// #region YVector
declare const YVectorKey: unique symbol;
interface YVector extends StaticAndInstance<typeof YVectorKey> {
staticFields: {
yCoord: number;
}
instanceFields: {
yLength: number;
}
symbolKey: typeof YVectorKey;
}
const Mixin_YVector: SubclassDecorator<YVector, typeof MixinBase, [number]> = function(
yCoordStatic: number
)
{
return function(
this: void,
_class: typeof MixinBase,
)
{
return class extends _class {
static yCoord = yCoordStatic;
yLength = 4;
}
}
}
// #endregion YVector
/*
const XYVector =
@Mixin_XVector
@Mixin_YVector(7)
class extends MixinBase {
};
*/
const XYVector = MultiMixinBuilder<[
XVector, YVector
], typeof MixinBase>
(
[
Mixin_XVector, Mixin_YVector(7)
], MixinBase
);
const xy = new XYVector;
it("xy", () => {
expect(xy.xLength).toBe(0);
expect(xy.yLength).toBe(4);
});
it("XYVector", () => {
expect(XYVector.xCoord).toBe(12);
expect(XYVector.yCoord).toBe(7);
});Without MultiMixinBuilder. the xLength and yLength properties of xy would be unknown to TypeScript. Likewise, TypeScript wouldn't know about XYVector.xCoord or XYVector.yCoord.
npm install --save-dev mixin-decorators
- A class decorator type which is aware of the new context argument. TypeScript 5.0's built-in
ClassDecoratortype won't work..ClassDecoratorFunctionfills the bill. - Classes have static fields, which means a special type to define the static and instance fields of a subclass.
StaticAndInstancedefines this. - Without depending on
StaticAndInstance, we need a type to define how a mix-in class joins the base class and its subclass's static and instance fields.MixinClassis a little convoluted, but works well. - Combining a base class with
StaticAndInstanceandClassDecoratorFunctionoffers aSubclassDecoratortype. An array ofStaticAndInstancetypes gives rise to aSubclassDecoratorSequencetype in the same file. - MultiMixinClass defines a
MixinClasstype from an array ofStaticAndInstanceobjects.
How do I use these types?
MultiMixinBuildercombines all of the above:- It takes a type parameter,
Interfaces, which is an ordered array ofStaticAndInstancetypes. - It takes a parameter,
decorators, which is aSubclassDecoratorSequencemapping theInterfacestoSubclassDecoratorinstances. - It also takes a parameter,
baseClass, which must be an subclass ofMixinBase. - It returns a
MultiMixinClassfrom the base class and invoking all theSubclassDecoratorfunctions.
- It takes a type parameter,
- The ordering of decorators in
MultiMixinBuilderdetermines the chain of derived-to-base classes.