Skip to content

Rules of ts2

Each rule names its diagnostic code; a few are enforced by the type checker, which names no ts2 code. A cell is a declared piece of state; a region is the part of a tree a condition decides.

A component’s name starts with a capital letter and its return type is Component. A tag whose function returns anything else is refused by the type checker.

A component returns exactly one element, or null. Wrap several in a <layer> when the grouping means something, or in <>…</> when it does not; null is the markup that draws nothing.

A component declared inside another captures nothing and reads no state. (TS2E225 for state; the type checker for a capture) Move it to the top of the file.

A component that reads state does not put a component that itself reads state on one arm of its root conditional. Wrap the conditional in a <layer>:

refused/conditional-rendering.ts2
const Toggle = (): Component => {
const [open, setOpen] = useState(0);
return open === 1
? <Counter />
: <text w={120} h={16} text="Show counter" font={Fonts.PLAIN} color={Colors.DIM}
onClick={() => { setOpen(1); }} />;
};

A prop a component does not declare lands on its root element. Geometry, hide, options, hover, a ref — anything the root accepts. The caller wins, and overriding something the component set for itself warns (TS2E233).

A handler does not fall through, and is not a prop. (TS2E202 on the tag; TS2E101 at a declaration) Bind the handler on an element the component draws, or on a <layer> around the call:

refused/rules-handler.ts2
{/* refused: a handler does not fall through, and is not a prop */}
<CheckboxRow label="Shift-click to drop" on={shiftDrop}
onClick={() => { setShiftDrop(1 - shiftDrop); }} />

Fallthrough needs one root the compiler can see. (TS2E202) A component that returns a fragment, or returns from two places, has no single root for a prop to land on; declare the prop instead.

A prop default is a literal. (TS2E209 at the call) color = 0xffffff works; color = Colors.WHITE leaves the prop required.

A Ref prop is required, never optional. (TS2E231)

refused/props.ts2
const Dimmer = ({ target }: { target?: Ref }): Component => (

A ref you were handed is not re-bound. (TS2E230)

refused/refs-page.ts2
const BoundByCallee = ({ target }: { target: Ref }): Component => (
<rect ref={target} w="fill" h={16} color={Colors.AMBER} fill />
);

Only <layer> may contain children. (type checker) Everything else draws one thing. Group with a layer.

A <text> or <input> has a font. (TS2E219) Without one it draws nothing.

Every ref is bound to exactly one element. (TS2E215)

A ref’s geometry is read below its binding. (TS2E215) Read it in a prop further down, or in a handler.

onX={null} alone is refused. (TS2E206) null takes a handler off where one may be bound: on one arm of a conditional, or through a ref.

hover is not combined with onMouseEnter or onMouseLeave. (TS2E217)

A position or a size written through ref.props is a pair. (TS2E209) ref.props = { w: 'fill', h: 40 }, never { h: 40 } alone; ref.height = 40 alone pins the width at its current pixels.

static sits in a tree that is data, and directly inside <ui> only on a <layer>. (TS2E224, TS2E302)

refused/interfaces-and-slots.ts2
const Poisoned = (): Component => {
const poison = useServerState(Varps.POISON);
return (
<layer w="fill" h="fill">
<rect static ref={wash} w="fill" h="fill" color={Colors.RED} fill transparency={200} hide />

Nothing that must run sits in a zero-size box. (TS2E252) A component reading state, a timer or a watch inside a box 0 wide or 0 tall never updates. Give the box w={1} h={1}, or a ref if a handler sizes it later:

refused/how-updates-happen.ts2
const Dead = (): Component => (
<layer w="fill" h="fill">
<layer w={0} h={0}>
<Count />
</layer>
</layer>
);

rebuildOn goes on an element with a region under it, and its key is an Int. On a component call it gates nothing (TS2E259); on an element with no {cond && …} below it, the same (TS2E256); in a component with no in-place update (TS2E257); a String key (TS2E258). A region gated on a counter that only goes up — {build > 0 && …} — is never re-created (TS2E255, a warning); add rebuildOn={build}.

refused/rules-rebuild-on.ts2
{/* refused: a component call has no region of its own */}
<Rows n={n} rebuildOn={n} />

Conditions are comparisons. (TS2E110, TS2E111) Write stock > 0, not stock. There is no truthiness, and ! applies only to a Boolean.

A comparison is a Boolean, and a Boolean is a value. It can be named, passed as a prop, and written into hide — hide={n === 0} is ordinary.

Use === and !==. (TS2E011)

Two strings are not compared with ===. (TS2E003) Test a string cell by its length, or compare an Int that stands for it:

refused/rules-strings.ts2
text={chosen === 'Wield' ? 'Wielded' : 'Not yet'} // refused

A constant array is reached with .map(), never by index or by length. (TS2E003) Where you need one entry, give it a name; where you need the count, it is a collection:

refused/rules-length.ts2
<text w="fill" h={16} text={`${TABS.length} tabs`} font={Fonts.PLAIN} /> // refused

A read that can miss says what to do about it. (type checker) .at(i) and .get(k) are Value | null unless the index is a literal; ?? fallback discharges a scalar, === null a record.

Game values are read through a hook, using a reference from a dictionary. (TS2E123) Give the reference to the hook, never as a prop; pass the level down, not the skill.

event.subId is an address, not a row. (TS2E132) Arithmetic on it is refused; a .map() callback’s index is the row.

A function is not a value. (TS2E101; TS2E120 for a call through a stored one) It cannot be stored, passed, or returned; a handler is written where it is bound, and a child reaches a component the caller built through a Ref prop.

State declarations live at the top level of a file. (TS2E124)

A useState initial value is a literal. (TS2E124) An identifier there means the other overload — the cell declared under that name — so useState(SECONDS) asks for shared state called SECONDS:

refused/rules-initial.ts2
const [left, setLeft] = useState(SECONDS); // refused: write useState(60)

A private cell holds an Int. (TS2E133) useState('') is refused: a private cell holds a whole number. A string cell is a module-level sessionState('') or persistedState('', key), and a component polls one such string (TS2E136):

refused/interface-state.ts2
const [name, setName] = useState(''); // ints only

A cell written and never read is refused. (TS2E228)

refused/watching-and-reacting.ts2
const [, setSeen] = useState(0); // never read
useWatch(Varps.POISON, () => { setSeen(1); });

A read-once is read in a render body, once per component, around no other hook. Not outside a render (TS2E236), no hook inside it (TS2E237), not in a .map() row (TS2E238), and not a cell with no setter (TS2E239):

refused/read-once-and-derived.ts2
const [poison] = useState(useServerStateOnce(Varps.POISON)); // no setter

A useDelay does not sit in a component that reads state. (TS2E218) Put it in a child that reads nothing, or use useInterval.

A persisted key belongs to exactly one setting. (TS2E126)

An interface is returned from a function that takes no parameters. (TS2E301) A value the server wants a render to have goes through a cell an exported function writes.

A <ui>’s geometry is a constant. (TS2E301) A literal, an enum member, a plain const, a field of a plain object — not PLATE_W + 20, and never { fill: '10%' }.

A value the server keys on is an enum tagged @wire, every member written out. (TS2E340) A positional, computed, negative or duplicate member is refused.

refused/server-entry-points.ts2
/** @wire */
export enum Delivery {
NONE = 0,
SENT, // refused: its value would be its position
ARRIVED = 2,
}

A game has one defineEngineHooks, and a hook holds no state and draws nothing. (TS2E351, TS2E355) Call a function that writes cells.

No two functions in one file differ only in case. (TS2E128) Rename counter beside Counter.

Props are read-only. A value that changes over time is a cell, and lives in whichever component owns it.

Compute what can be computed. If a value follows from other values, work it out rather than storing it as well.