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/exportin the entry or any part (they must stay global scripts; a module would hide$gfrom the other parts); - function declarations hoist across the whole concatenation,
consts do not — keep the$gdeclaration 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.