Skip to main content

Advanced Mixin Techniques

On the previous page, we learned about a simple mixin tailored specifically for Component. However, within the GeaJS ecosystem, you may also want to extend plain objects such as Store and Object. On this page, let’s learn a general-purpose approach to address those needs.

First, let's define a generic AnyConstructor. It looks like this:

export type AnyConstructor = new (...args: any[]) => any;

Looking at this, you might think that returning any isn't type-safe.
However, this isn't a problem. Type inference works correctly for each mixin. Why is that? Let's think about it for a moment.

First, in this context, any does not suppress type information but simply passes it through as-is. Let’s consider this using a real-world scenario. At a shipping company, an inspector is responsible for checking incoming packages: they won’t let anything through that violates the rules, or if removing part of the contents would allow it to pass, they’ll remove that portion. A toolbox named Component arrives. The inspector checks it, but the rule is any, so they must let anything through. In other words, they won’t send the Component back, nor will they remove any of its contents. Consequently, the tools inside—named dispose() and template()—passed through without being removed. In this way, you can define a generic constructor that preserves all the original types and maintains type inference.

Short Story

It’s debatable whether any can really be considered an inspection rule, but this was the only good example I could come up with, so please bear with me.

Why Not object or {}?​

You might wonder why we don't use a cleaner type like object or {} instead of any:

// This will destroy your type completion
type ObjectConstructor = new (...args: any[]) => object;

If the inspector's rule is object, the inspector behaves differently. In TypeScript, object means "an anonymous entity with no properties." The inspector looks at the Component toolbox and says, "Okay, this is an object, but to make it fit our rule, I must strip away all its internal tools."

As a result, your template() and created() methods are completely removed, and the type is downgraded to an empty object. To prevent this and preserve 100% of the original types, using any as a transparent filter is the best practice for general-purpose mixins.

Example​

export interface XxxMixin {
// ...
}

export function withXxx<TBase extends AnyConstructor>(Base: TBase) {
// ...
}

This allows you to create a generic mixin that can be used with plain classes, Stores, and Components.

Defensive Programming in dispose​

When creating a general-purpose mixin, you don't know what class will be passed in as Base. If a developer passes a plain JavaScript class like Object, it won't have a dispose() method.

By adding the if (typeof super.dispose === 'function') check, you prevent the application from completely crashing due to an undefined call, ensuring that the cleanup chain successfully reaches the GeaJS core.

Of course, regardless of the type of mixin, it’s best to have a dispose method defined in the class, as this is the cleanest way to free up memory. However, if—for some reason—a mixin lacks a dispose method, calling super.dispose() will cause the application to crash. Therefore, as a precaution, writing if (typeof super.dispose === `function`) won’t prevent memory leaks, but it will prevent the risk of the application crashing immediately. So, from a practical standpoint, it’s a good idea to include this code. This is what’s known as defensive programming.

When creating mixins like this, there are types—such as Constructor—and methods—such as dispose()—that are common or should be defined consistently. Copying and pasting these definitions, or writing them out by hand to ensure you don’t forget them without type hints, is a subtle but burdensome task. So, on the next page, let’s learn how to solve this problem.