Created
November 18, 2025 05:43
-
-
Save janis-me/98e14a8df963a5a67d0ab76e98790c5e to your computer and use it in GitHub Desktop.
Typescript generic class mixins
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| /** | |
| * 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