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.
Components
Section titled “Components”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>:
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: 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)
const Dimmer = ({ target }: { target?: Ref }): Component => (A ref you were handed is not re-bound. (TS2E230)
const BoundByCallee = ({ target }: { target: Ref }): Component => ( <rect ref={target} w="fill" h={16} color={Colors.AMBER} fill />);Markup
Section titled “Markup”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)
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:
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: a component call has no region of its own */}<Rows n={n} rebuildOn={n} />Values
Section titled “Values”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:
text={chosen === 'Wield' ? 'Wielded' : 'Not yet'} // refusedA 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:
<text w="fill" h={16} text={`${TABS.length} tabs`} font={Fonts.PLAIN} /> // refusedA 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:
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):
const [name, setName] = useState(''); // ints onlyA cell written and never read is refused. (TS2E228)
const [, setSeen] = useState(0); // never readuseWatch(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):
const [poison] = useState(useServerStateOnce(Varps.POISON)); // no setterA 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)
Interfaces and the game
Section titled “Interfaces and the game”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.
/** @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.
Projects
Section titled “Projects”No two functions in one file differ only in case. (TS2E128) Rename
counter beside Counter.
Two the compiler leaves to you
Section titled “Two the compiler leaves to you”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.