Configurable Method Names for Ecosystem Interoperability
Symbol-Keyed Internal State solved collisions for a mixin's internal tracking state. Public methods have the opposite problem: they're supposed to be visible and callable, so a Symbol isn't an option — and that's exactly where a different kind of collision risk shows up.
Why This Matters at the Ecosystem Level
The risk here isn't about one project using the same mixin twice — it's about two independently-published libraries choosing the same method name. createQuery is the natural name for "a mixin that creates a query." If a second, unrelated query library shows up in the Gea ecosystem someday, there's a real chance it reaches for that exact same name, because it's the obvious choice for anyone solving that problem. If both mixins hardcode createQuery, a project that wants features from both simply can't compose them — one silently overwrites the other on the resulting class, with no error and no warning.
@geastack-community/query's withQuery addresses this preemptively by making its public method name a configurable parameter of the mixin factory itself, rather than a hardcoded string.
The Shape
export type WithQueryMixin<K extends string = 'createQuery'> = {
[P in K]: <TData>(
queryKey: string,
queryFn: () => Promise<TData>,
options?: GeaQueryOptions
) => GeaQuery<TData>
} & {
dispose(...args: any[]): void
}
export function withQuery<
TBase extends Constructor<Component>,
K extends string = 'createQuery'
>(
Base: TBase,
creatorName: K = 'createQuery' as K
) {
const Derived = class extends Base {
[managedQueries]: GeaQuery[] = []
[creatorName]<TData>(queryKey: string, queryFn: () => Promise<TData>, options?: GeaQueryOptions): GeaQuery<TData> {
const query = new GeaQuery(queryKey, queryFn, options)
this[managedQueries].push(query)
return query
}
dispose() {
this[managedQueries].forEach(query => query.destroy())
this[managedQueries] = []
super.dispose()
}
}
return Derived as unknown as TBase & (new (...args: any[]) => WithQueryMixin<K>)
}
Three pieces work together here:
K extends string = 'createQuery'— a generic type parameter for the method name, defaulting to the obvious name so the common case (this library'swithQuery, used once, with nothing else to conflict with) needs no extra configuration.WithQueryMixin<K>uses a mapped type ([P in K]: ...) to describe "an object with exactly one method, whose name is whateverKis." This is what makes the type of the generated method follow the runtime name — callers get full autocomplete and type-checking on whatever name they chose, not just on the default.[creatorName]<TData>(...)— a computed method name inside the class body itself, using the runtime string value passed in, so the method is actually defined under that name on the resulting class.
Since the names of creators like createQuery are reused,
const creator = 'createQuery';
it’s a good idea to declare them this way and use them. This makes the code even cleaner. When using this approach,
K extends string = 'createQuery';
for type definitions,
K extends string = typeof creator
Otherwise, you’ll be trying to insert a value where a type should be, which will result in a syntax error.
Using It
The default case needs nothing extra:
class UserProfile extends withQuery(Component) {
user = this.createQuery('user', () => fetch('/api/user').then(r => r.json()))
}
The parameter earns its keep the moment a second, differently-sourced mixin needs to coexist on the same class under what would otherwise be an identical method name:
import { withQuery as withGeaQuery } from '@geastack-community/query'
import { withQuery as withOtherQuery } from 'some-other-query-library'
class Dashboard extends withMixins(
(Base) => withGeaQuery(Base, 'createGeaQuery'),
(Base) => withOtherQuery(Base, 'createOtherQuery'),
Component
) {
a = this.createGeaQuery('a', fetchA)
b = this.createOtherQuery('b', fetchB)
}
Note what aliasing the import does and doesn't do here: withQuery as withGeaQuery only renames the local binding for the mixin function — it has no effect on the method name that function installs on the class. Without a creatorName-style parameter built into the mixin itself, two same-named methods from two different libraries have no way to coexist, no matter how the imports are aliased.
Since withMixins (see withMixins) expects each argument to be a function taking a single Base parameter, wrapping withQuery(Base, name) in an arrow function is what lets the second argument (the desired method name) be supplied at each composition site.
A Standard Part of Any Mixin's Public API
There's no reliable way to predict which method names will and won't collide with some future, unrelated library. A name that looks specific and safe today can turn out to be the obvious choice for someone else solving a similar problem tomorrow — and a mixin author has no visibility into what the rest of the ecosystem will eventually build.
Given that the parameter costs almost nothing for the common case — it's optional, defaults to the obvious name, and existing call sites need no changes — the practical guidance is simple: any mixin that adds a public method should accept a configurable name for it, the same way withQuery does. Treat this as a standard part of the mixin-authoring checklist alongside Symbol-keyed internal state and a proper dispose() chain, not as an optional extra reserved for mixins that seem especially collision-prone.