Skip to content

Instantly share code, notes, and snippets.

@janis-me
Created November 18, 2025 05:43
Show Gist options
  • Select an option

  • Save janis-me/98e14a8df963a5a67d0ab76e98790c5e to your computer and use it in GitHub Desktop.

Select an option

Save janis-me/98e14a8df963a5a67d0ab76e98790c5e to your computer and use it in GitHub Desktop.
Typescript generic class mixins
/**
* A generic constructor type, that returns an instance of type T
*/
type Constructor<T = {}> = abstract new (...args: any[]) => T;
/** Dummy type that is passed through surimi classes to showcase keeping types. */
type SurimiRoot = Record<string, object>;
/**
* A mixin function type that takes a base class and returns an extended class.
*/
type MixinFunction<TClass> = <TBase extends Constructor<TClass>>(
base: TBase,
plugins: Plugin[]
) => Constructor<TClass>;
export interface Plugin {
selector: MixinFunction<SelectorBase<string>>;
}
/**
* Extracts the mixin classes from the provided plugins as an array of types, for the specified key.
* Used to preserve the types of the mixins for further use.
*/
type GetMixinClasses<
TKey extends keyof Plugin,
TPlugins extends Plugin[],
TBase = {}
> = TPlugins extends readonly [infer First, ...infer Rest]
? First extends Plugin
? Rest extends Plugin[]
? GetMixinClasses<
TKey,
Rest,
TBase &
(First[TKey] extends MixinFunction<infer TClass> ? TClass : {})
>
: TBase & (First[TKey] extends MixinFunction<infer TClass> ? TClass : {})
: TBase
: TBase;
abstract class SurimiBase<
TBuildRes extends string | Record<string, unknown> = string
> {
protected _root: SurimiRoot;
public constructor(root: SurimiRoot) {
this._root = root;
}
public abstract build(): TBuildRes;
}
abstract class SelectorBase<TCtx extends string> extends SurimiBase<string> {
protected _selector: TCtx;
public constructor(root: SurimiRoot, selector: TCtx) {
super(root);
this._selector = selector;
}
public build(): string {
return "";
}
}
export function createSelector<TCtx extends string, TPlugins extends Plugin[]>(
selector: TCtx,
root: SurimiRoot,
plugins: TPlugins
) {
let PluginBase = SelectorBase<TCtx>;
// Apply each plugin
for (const plugin of plugins) {
PluginBase = plugin.selector(PluginBase, plugins) as Constructor<
SelectorBase<TCtx>
>;
}
class SelectorBuilder extends PluginBase {
constructor() {
super(root, selector);
}
public select<TCtx extends string>(ctx: TCtx) {
return createSelector(ctx, this._root, plugins);
}
}
return new SelectorBuilder() as unknown as GetMixinClasses<
"selector",
TPlugins,
SelectorBase<TCtx>
>;
}
export function create<TPlugins extends Plugin[]>(...plugins: TPlugins) {
return {
select: <TCtx extends string>(selector: TCtx) => {
return createSelector(selector, {}, plugins);
},
};
}
const SelectWithDashPlugin = {
selector: (base, plugins) => {
abstract class WithDash extends base {
public dash(ctx: string) {
return createSelector(`-${ctx}`, this._root, plugins);
}
}
return WithDash;
},
} satisfies Plugin;
const SelectAllPlugin = {
selector: (base, plugins) => {
abstract class WithDash extends base {
public all() {
return createSelector(`*`, this._root, plugins);
}
}
return WithDash;
},
} satisfies Plugin;
const { select } = create(SelectWithDashPlugin, SelectAllPlugin);
// Should return a class that extends SelectorBase and all plugins, in this case with the `dash` method
const x = select("test").dash("a");
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment