Поведенческие контракты JS

В JavaScript нет интерфейсов как в TypeScript/Java, но язык опирается на набор "поведенческих контрактов": Callable, Thenable, Iterable, Async Iterable, Array-like, Observable, Streamable, Disposable. Каждый — это договор о наличии определённых методов/символов.

Что это / Зачем

Языковые конструкции (await, for-of, for await-of, ...spread, using) полагаются не на конкретный класс, а на контракт. Если ваш объект соответствует контракту — он вписывается в синтаксис. Это глубокий duck typing, удобный для интероперабельности библиотек без наследования.

Контракты

Контракт Метод/символ Где используется
Callable `` (любая функция) fn, Function.prototype.call/apply
Thenable .then(onF, onR) await, Promise.resolve
Iterable [Symbol.iterator] for-of, ...spread, [...obj]
Async Iterable [Symbol.asyncIterator] for await-of
Array-like length + индексы Array.from(obj), Array.prototype.slice.call(obj)
Observable .subscribe(observer) RxJS, библиотеки реактивности
Streamable events data/end + pipe Node streams, ReadableStream
Disposable [Symbol.dispose] или [Symbol.asyncDispose] using x = ... (ES2023)

Ключевые моменты

  • Контракты дополняют, не конкурируют: Promise — это Thenable. Array — Iterable + Array-like. Generator — Iterable.
  • await ищет только .then — поэтому работает с любым thenable, не только с Promise.
  • for-of ищет [Symbol.iterator], for-await-of — оба (предпочитает asyncIterator).
  • Array-like ≠ Iterable: старый объект arguments array-like, но не iterable (поэтому нужен Array.from или rest-параметры).
  • Observable стандартизирован в нескольких proposals, но в спеку не вошёл. RxJS — де-факто стандарт.
  • Сигналы (Angular Signals, React, SolidJS) вытесняют Observable в новых фреймворках — они проще и не требуют RxJS-операторов.
  • using x = makeResource (ES2023, TC39 Stage 4) вызывает x[Symbol.dispose] при выходе из scope — аналог Python with, C# using.

Подводные камни

  • Не называйте свои методы then / catch в обычных бизнес-объектах — await obj нечаянно вызовет ваш then.
  • instanceof не работает между контекстами (iframe, vm.runInContext) — лучше проверять контракт (typeof x.then === 'function').
  • Symbol.dispose и using — ещё новые, требуют Node 22+ или транспиляции.
  • Не наследовать User от EventEmitter — это композиция через has-a, а не is-a.

🎓 Источники

  • 🎓 [JavaScript Callable, Thenable, Promise, Array-like, Iterable, Observable, Streamable] · 2025-10-24 · YouTube
    • Тезисы:
      • Callable — контракт "оператор вызова ``". Proxy apply срабатывает только если target — callable.
      • Error-first: не больше двух параметров в callback, null вместо undefined для ошибки.
      • Не сводить всё к undefined — Infinity, NaN, специальные значения часто читаемее, чем union-типы.
      • Promise строже чем thenable: гарантирует асинхронность колбэка, однократный resolve, ассимиляцию thenable.
      • Сигналы проще RxJS — большинству приложений не нужны операторы вроде mergeMap.
      • Стримы в object mode — гибридный контракт между Streamable и Iterable.
      • Disposable / using — финализационный код в классе, аналог Python with.
    • Цитата:

      «Не наследовать User от EventEmitter — это композиция через has-a, а не is-a. Наследование нужно, но редко.»

  • 🎓 [Explicit resource management with Using and Symbol.dispose] · 2025-05-22 · YouTube
    • Тезис: using корректно отрабатывает финализатор даже при exception, в отличие от ручного try/finally.

См. также