Docs

TypeScript authoring

Typed smart apps with the ambient declarations, the $g pattern and the multi-file parts layout the built-ins use.

The hub runs plain JavaScript, but you can author in TypeScript and compile with tsc or esbuild (no bundling, no module wrapper: the output must stay a script). The built-in apps are written this way.

Getting the types

The platform's ambient declarations (ambient.d.ts) describe every global function, the EntityWrapper class, EntityChangeEvent, and the SmartApp<Inputs, State> helper type. They are not yet published as an npm package; copy the file into your project for now. The same declarations drive the reference section.

The $g pattern

Because a smart app is a script, its exports are properties of globalThis. The built-ins give globalThis a precise type once and then assign through it:

import type { SmartApp } from './ambient';

const $g = globalThis as unknown as SmartApp<
  {                                  // inputs, by preference key
    motionSensor: Entity;
    lights: Entity | Entity[];       // multiple: true
    offAfterMinutes: number;
    logging: boolean;
  },
  {                                  // persisted state
    motionCount?: number;
    lastMotionAt?: string;
  }
>;

$g.definition = { name: 'Simple Motion Light', namespace: 'com.example', category: 'lighting' };
$g.preferences = { pages: [ /* … */ ] };

$g.installed = function installed() { initialize(); };
$g.updated = function updated() { unsubscribe(undefined); unschedule(undefined); initialize(); };

function initialize(): void {
  subscribe($g.motionSensor, { to: 'on' }, onMotion);
}

function onMotion(evt: EntityChangeEvent): void {
  $g.state.motionCount = ($g.state.motionCount ?? 0) + 1;
  const lights = Array.isArray($g.lights) ? $g.lights : [$g.lights];
  for (const l of lights) void l.turnOn();
}

Inputs is a required type argument so a forgotten declaration is a compile error instead of a silent unknown. Handlers stay top-level function declarations so the hub can find them by name.

Compiling

npx esbuild src/my-app.ts --format=iife --target=es2022 --outfile=dist/my-app.js
# or: npx tsc --target es2022 --module none …

Check the output has no import / export and no helper wrapper that renames your top-level functions; esbuild --format=iife is fine because it keeps function declarations as-is inside a single scope, but make sure the hub can still see them on globalThis — the safest output is plain transpilation (esbuild --format=esm with no imports, or tsc).

Splitting a large app into parts

The built-in SimpleRule is authored as an entry file plus simple-rule/*.part.ts files that the build concatenates in filename order (10-form, 20-lifecycle, …) before transpiling. The rule set that makes that work:

  • no import / export in the entry or any part (they must stay global scripts; a module would hide $g from the other parts);
  • function declarations hoist across the whole concatenation, consts do not — keep the $g declaration first;
  • give helpers app-specific names and never reuse DOM global names (name, status, open, event, …).

The authoring guide in the platform repo (builtin/README.md) has the full set of conventions.

On this page