Skip to main content

@Partial

The @Partial() decorator implements a TypeScript mixin pattern similar to C# partial classes. It allows a class to be composed from multiple source classes, with methods from each mixin copied onto the target prototype.

Package: io-oidis-commons

import { Partial } from "@io-oidis-commons/.../Primitives/Decorators.js";

Syntax​

@Partial(MixinClassA, MixinClassB, MixinClassC)
export class CompositeClass extends BaseClass {
// Methods from MixinClassA, MixinClassB, MixinClassC are
// copied onto CompositeClass.prototype at runtime
}

How It Works​

  1. @Partial(ClassA, ClassB) registers the mixin list in globalThis.oidisPartials
  2. At runtime, Reflection.resolvePartials() iterates the registered partials
  3. Methods from each mixin class are copied onto the target class prototype
  4. The target class gains all methods from all mixins as if they were defined directly

Primary Use Case: AuthManager​

The canonical example in Oidis is the Hub's AuthManager, which is composed from 11 partial classes:

@Partial(
UsersManager,
GroupsManager,
BasicAuthenticationAPI,
TwoFactorAuthenticationAPI,
OAuth2Helper,
PasswordResetManager,
SessionManager,
RegistrationManager,
PermissionsManager,
TokenManager,
AuditManager
)
export class AuthManager extends BaseAuthManager {
// AuthManager has all methods from all 11 mixins
// Each mixin handles a specific auth concern
}

This keeps the AuthManager file manageable while providing a single composite class to the rest of the application.

Typing Delegates vs Runtime Injection​

Composite classes commonly pair the decorator with typing delegates — class fields that point at the mixin prototype methods:

@Partial(CalendarsHandler, ImportsHandler, ExportsHandler /* , ... */)
export class CalendarManager extends BaseConnector {
public ImportCSV = ImportsHandler.prototype.ImportCSV;
public GetEconomicSummary = ExportsHandler.prototype.GetEconomicSummary;
}

These delegates exist only for TypeScript (IntelliSense, type-checking of this.ImportCSV(...) call sites). At runtime they are instance fields assigned in the constructor — the methods available on CompositeClass.prototype come solely from the @Partial(...) argument list via resolvePartials().

Adding a new mixin — both places, always

When adding a new handler class you must update both the @Partial(...) argument list and the typing delegates. A delegate without the decorator entry compiles fine but leaves CompositeClass.prototype.<Method> undefined at runtime. Typical symptom: server startup crash while registering authorized methods —

[ERROR] Failed to init auth system. Cannot read properties of undefined (reading 'name')
at AuthMethodsManager ... generateAuthMethodsFrom

because generateAuthMethodsFrom(CompositeClass, (proto) => [proto.<Method>, ...]) reads methods from the prototype, where only @Partial-injected methods exist.

@Route and @Partial​

When a partial mixin has @Route decorated methods, the route resolution uses __partialTarget to resolve the ClassName back to the composite class:

// In a partial mixin:
export class BasicAuthenticationAPI {
@Route("POST /api/auth/login")
public async Login($credentials : ICredentials) : Promise<IAuthResult> {
// Route resolves to AuthManager (the composite), not BasicAuthenticationAPI
}
}

Rules​

  • Partials are resolved once at startup
  • Method name conflicts: methods are copied only when the target prototype does not already own them (hasOwnProperty check in resolvePartials()) — so the target's own methods always win and among mixins the first one in the list wins
  • A mixin class can be injected into only one composite (__partialTarget guard throws on the second injection)
  • Works on both backend and frontend classes
  • Mixins should be standalone classes — avoid inter-mixin dependencies

When to Use​

@Partial is designed for large service classes that would otherwise become unwieldy. Consider it when:

  • A class exceeds ~500 lines with distinct functional areas
  • Multiple developers need to work on different aspects of the same class
  • The class has clear, separable concerns (auth, users, sessions, etc.)

For simpler cases, prefer standard composition or inheritance.


See also: @Extern | @Route | Architecture Overview | Reflection (resolvePartials mechanism)