@bind Decorator

Lazily binds a class prototype method to the instance (this) the first time it is accessed, avoiding the need for manual constructor binding while preserving prototype memory efficiency.


Why

Common patterns like passing methods as callbacks (event listeners, timers, Promise chains) require stable this binding. Manual solutions:

@bind offers a declarative, lazy alternative:


Quick Start

import {bind} from '@exadel/esl/modules/esl-utils/decorators';

class ButtonController {
  clicks = 0;

  @bind
  handleClick(evt: Event) {
    this.clicks++;
  }

  attach(el: HTMLElement) {
    el.addEventListener('click', this.handleClick); // always correctly bound
  }
}

API

function bind(target: object, key: string, descriptor: TypedPropertyDescriptor<Function>): TypedPropertyDescriptor<Function>;

Applied to: class prototype methods (not getters, not setters, not fields).

Result

The original descriptor is replaced with an accessor descriptor:


Behavior Details

  1. Decoration phase: method value captured (originalFn).
  2. A getter is defined. On first instance access:
    • Detects access via instance (not prototype).
    • Binds originalFn with this.
    • Defines the bound function directly on the instance (overwriting the accessor at instance level only).
  3. Future accesses hit the already bound instance property (no more indirection).
  4. Access using Class.prototype.method still returns the unbound original (useful for testing or chaining decorators).

Examples

Event Handler

class Modal {
  @bind onEsc(e: KeyboardEvent) { if (e.key === 'Escape') this.close(); }
  open() { document.addEventListener('keydown', this.onEsc); }
  close() { document.removeEventListener('keydown', this.onEsc); }
}

With Other Decorators

Order matters when stacking decorators that wrap execution (e.g. memoization). @bind only adjusts access / binding and does not change the call semantics of the wrapped function itself. Place @bind outermost (lowest in code) if the method result should reflect wrapper transforms before binding.

class Service {
  @bind
  @memoize()
  heavy() { /* ... */ }
}

(Here the memoized wrapper is what gets bound.)


Limitations

Aspect Status Rationale
Getters/Setters Not supported Binding semantics differ; omitted for clarity.
Fields / Arrow props Not supported They aren’t on the prototype; binding unnecessary.
Re-binding after prototype mutation Not handled Complexity vs low practical need.
Symbol method names Works Standard property descriptor handling.

Typing Notes

The decorator preserves the original method signature. No return type widening occurs. Works with this typing as originally declared.


Best Practices