A type that represents a function with the Result
return type.
Type helper for mixins creation. Supports up to 5 class constructor arguments. May lead to compilation errors in some edges cases. See the Mixin for details.
This function allows you to create mixin classes. Mixin classes solves the well-known problem with "classical" single-class inheritance, in which class hierarchy must form a tree. When using mixins, class hierarchy becomes an arbitrary acyclic graph.
Another view on mixins is that, if "classical" class is a point (a vertice of the graph), mixin class is an arrow between the points (an edge in the graph, or rather, a description of the edge).
Some background information about the mixin pattern can be found here and here.
The pattern, being described here, is the evolution of the previous work, and main advantage is that it solves the compilation error for circular references.
The pattern looks like:
class Mixin1 extends Mixin(
[],
(base : AnyConstructor) =>
class Mixin1 extends base {
prop1 : string
method1 () : string {
return this.prop1
}
static static1 : number
}
){}
The core of the definition above is the mixin lambda - a function which receives a base class as its argument and returns a class, extending the base class with additional properties.
The example above creates a mixin Mixin1
which has no requirements. Requirements are the other mixins,
which needs to be included in the base class of this mixin.
There's also a special type of the requirement,
called "base class requirement". It is optional and can only appear as the last argument of the requirements
array. It does not have to be a mixin, created with the Mixin
function, but can be any JS class. This requirement
specifies, that the base class of this mixin should be a subclass of the given class (or that class itself).
The requirements of the mixin needs to be listed 3 times:
Mixin
functionFor example, Mixin2
requires Mixin1
:
class Mixin2 extends Mixin(
[ Mixin1 ],
(base : AnyConstructor<Mixin1, typeof Mixin1>) =>
class Mixin2 extends base {
}
){}
And Mixin3
requires both Mixin1
and Mixin2
(even that its redundant, since Mixin2
already requires Mixin1
,
but suppose we don't know the implementation details of the Mixin2
):
class Mixin3 extends Mixin(
[ Mixin1, Mixin2 ],
(base : AnyConstructor<Mixin1 & Mixin2, typeof Mixin1 & typeof Mixin2>) =>
class Mixin3 extends base {
}
){}
Now, Mixin4
requires Mixin3
, plus, it requires the base class to be SomeBaseClass
:
class SomeBaseClass {}
class Mixin4 extends Mixin(
[ Mixin3, SomeBaseClass ],
(base : AnyConstructor<
Mixin3 & SomeBaseClass, typeof Mixin3 & typeof SomeBaseClass
>) =>
class Mixin4 extends base {
}
){}
As already briefly mentioned, the requirements are "scanned" deep and included only once. Also all minimal classes are cached - for example the creation of the Mixin3 will reuse the minimal class of the Mixin2 instead of creating a new intermediate class. This means that all edges of the mixin dependencies graph are created only once (up to the base class).
Requirements can not form cycles - that will generate both compilation error and run-time stack overflow.
The typing for the Mixin
function will provide a compilation error, if the requirements don't match, e.g. some requirement is
listed in the array, but missed in the types. This protects you from trivial mistakes. However, the typing is done up to 10 requirements only.
If you need more than 10 requirements for the mixin, use the MixinAny function, which is an exact analog of Mixin
, but without
this type-level protection for requirements mismatch.
It is possible to simplify the type of the base
argument a bit, by using the ClassUnion helper. However, it seems in certain edge cases
it may lead to compilation errors. If your scenarios are not so complex you should give it a try. Using the ClassUnion helper, the
Mixin3
can be defined as:
class Mixin3 extends Mixin(
[ Mixin1, Mixin2 ],
(base : ClassUnion<typeof Mixin1, typeof Mixin2>) =>
class Mixin3 extends base {
}
){}
Note, that due to this issue, if you use decorators in your mixin class, the declaration needs to be slightly more verbose (can not use compact notation for the arrow functions):
class Mixin2 extends Mixin(
[ Mixin1 ],
(base : AnyConstructor<Mixin1, typeof Mixin1>) => {
class Mixin2 extends base {
@decorator
prop2 : string
}
return Mixin2
}
){}
As you noticed, the repeating listing of the requirements is somewhat verbose. Suggestions how the pattern can be improved are very welcomed.
instanceof
You can instantiate any mixin class just by using its constructor:
const instance1 = new Mixin1()
const instance2 = new Mixin2()
As explained in details here, mixin constructor should accept variable number of arguments
with the any
type. This is simply because the mixin is supposed to be applicable to any other base class, which may have its own type
of the constructor arguments.
class Mixin2 extends Mixin(
[ Mixin1 ],
(base : AnyConstructor<Mixin1, typeof Mixin1>) => {
class Mixin2 extends base {
prop2 : string
constructor (...args: any[]) {
super(...args)
this.prop2 = ''
}
}
return Mixin2
}
){}
In other words, its not possible to provide any type-safety for mixin instantiation using regular class constructor.
However, if we change the way we create class instances a little, we can get the type-safety back. For that,
we need to use a "uniform" class constructor - a constructor which has the same form for all classes. The Base class
provides such constructor as its static new method. The usage of Base
class is not required - you can use
any other base class.
The instanceof
operator works as expected for instances of the mixin classes. It also takes into account all the requirements.
For example:
const instance2 = new Mixin2()
const isMixin2 = instance2 instanceof Mixin2 // true
const isMixin1 = instance2 instanceof Mixin1 // true, since Mixin2 requires Mixin1
See also isInstanceOf.
You have defined a mixin using the Mixin
function. Now you want to apply it to some base class to get the "specific" class to be able
to instantiate it. As described above - you don't have to, you can instantiate it directly.
Sometimes however, you still want to derive the class "manually". For that, you can use static methods mix
and derive
, available
on all mixins.
The mix
method provides a direct access to the mixin lambda. It does not take requirements into account - that's the implementor's responsibility.
The derive
method is something like "accumulated" mixin lambda - mixin lambda with all requirements.
Both mix
and derive
provide the reasonably typed outcome.
class Mixin1 extends Mixin(
[],
(base : AnyConstructor) =>
class Mixin1 extends base {
prop1 : string
}
){}
class Mixin2 extends Mixin(
[ Mixin1 ],
(base : AnyConstructor<Mixin1, typeof Mixin1>) =>
class Mixin2 extends base {
prop2 : string
}
){}
const ManualMixin1 = Mixin1.mix(Object)
const ManualMixin2 = Mixin2.mix(Mixin1.mix(Object))
const AnotherManualMixin1 = Mixin1.derive(Object)
const AnotherManualMixin2 = Mixin2.derive(Object)
Using generics with mixins is tricky because TypeScript does not have higher-kinded types and type inference for generics. Still some form of generic arguments is possible, using the interface merging trick.
Here's the pattern:
class Duplicator<Element> extends Mixin(
[],
(base : AnyConstructor) =>
class Duplicator extends base {
Element : any
duplicate (value : this[ 'Element' ]) : this[ 'Element' ][] {
return [ value, value ]
}
}
){}
interface Duplicator<Element> {
Element : Element
}
const dup = new Duplicator<boolean>()
dup.duplicate('foo') // TS2345: Argument of type '"foo"' is not assignable to parameter of type 'boolean'.
In the example above, we've defined a generic argument Element
for the outer mixin class, but in fact, that argument is not used anywhere in the
nested class definition in the mixin lambda. Instead, in the nested class, we define a property Element
, which plays the role of the
generic argument.
Mixin class methods then can refer to the generic type as this[ 'Element' ]
.
The generic arguments of the outer and nested classes are tied together in the additional interface declaration, which, by TypeScript rules
is merged together with the class definition. In this declaration, we specify that property Element
has type of the Element
generic argument.
The most important limitation we found (which affect the old pattern as well) is the compilation error, which will be issued for the private/protected methods, when compiling with declarations emitting (*.d.ts files generation).
This is a well-known problem in the TypeScript world – the *.d.ts files do not represent the internal data structures of the TypeScript compiler well. Instead they use some simplified syntax, optimized for human editing. This is why the compiler may generate false positives in the incremental compilation mode – it uses *.d.ts files internally.
This can be a show-stopper for the people that use declaration files (usually for publishing). Keep in mind though, that you can always publish actual TypeScript sources along with the generated JavaScript files, instead of publishing JavaScript + declarations files.
This is an exact analog of the Mixin function, but without type-level protection for requirements mismatch. It supports unlimited number of requirements.
This is the instanceof
analog for the classes created with Mixin helper. It also provides typeguard.
There's no strict need to use it, as the native instanceof
is also supported for the mixins created with the Mixin helper and also provides
typeguarding.
Any value, normally an instance of the mixin class
The constructor function of the class, created with Mixin
Generated using TypeDoc
A type that represents a constructor function, that returns
Instance
type on instantiation. The properties of the function itself are typed withStatic
argument. These properties will correspond to the static methods/properties of the class.