Hem parses JSX directly and emits standard .tsx. React (or the configured JSX runtime) handles element and prop type checking, using jsxImportSource from the project’s tsconfig. Hem provides specific expression and attribute syntax within standard JSX.
Expressions in JSX holes
Conditional if
Because if is an expression in Hem, conditional elements evaluate to ?Element. If the condition is false, the expression renders nothing. Conditions must evaluate to a boolean; Hem does not support truthiness coercion.
let Notice = ({ count }: { count: number }) =>
<section>
{if count > 0 { <p>{count} new messages</p> }}
</section>This avoids issues with the JavaScript idiom {count && <p>…</p>}, which renders 0 when count is zero.
match in braces
Any Hem expression can be placed inside JSX braces, including exhaustive match expressions. Pattern matching is checked by the compiler before passing elements to React.
let Badge = ({ state }: { state: PaymentState }) =>
<span className={match state {
| Paid(_) => "green"
| Refunded(_) => "red"
| Pending => "gray"
}}>
{label(state)}
{match state {
| Pending => <span>Waiting</span>
| Paid(_) => <span>Paid</span>
| Refunded(_) => <span>Refunded</span>
}}
</span>The match expression uses standard Hem pattern matching rules, including exhaustive coverage checking for imported discriminated unions and supported structural records.
JSX attributes
Prop puns
An attribute written without a value passes the in-scope binding with the matching name:
<UserCard user compact subtitle?=bio onSelect=select(user.id) />This shorthand compiles to standard TSX attributes with expressions:
<UserCard
user={user}
compact={compact}
{...(bio === null || bio === undefined ? {} : { subtitle: bio })}
onSelect={select(user.id)}
/>Literals and postfix property or call expressions can omit braces (title=name, count=items.length, onSelect=select(user.id)). Expressions with operators must remain inside braces to avoid parsing ambiguity with JSX closing tokens (> and /).
Optional prop omission
attr?=value accepts a ?T value and omits the property from the emitted props object when absent. The shorthand attr? expands to attr?=attr.
Hem emits a conditional spread instead of attr={undefined}. This preserves compatibility with TypeScript’s exactOptionalPropertyTypes and ensures React default props still apply. This rule changes only the emitted object structure; it does not make a required prop optional in component types.
Explicit booleans
Because bare attribute names resolve to in-scope bindings, <input checked /> passes the local checked variable rather than the boolean literal true. Write boolean literals explicitly:
let checked = isSelected(item)
<input type="checkbox" checked={checked} disabled={true} />If an attribute has no matching in-scope binding, Hem reports a compile error. Write disabled={true} when passing literal boolean values.
Interop boundary
JSX structure and child nodes compile directly to TSX. Text nodes remain text, and expression containers use {}. Hem does not modify HTML tags, JSX runtime calls, component names, or prop definitions. The emitted .tsx file is checked directly by TypeScript against component declarations.
Hem components can import and export React components, third-party prop types, HTML elements, refs, and JSX runtime configurations. Use TypeScript for framework-specific JSX patterns not supported in Hem, while writing application logic in Hem.
Related diagnostics
| Code | Meaning |
|---|---|
| HEM2002 | A conditional expression is not boolean. |
| HEM2005 | A match in a JSX hole is missing coverage or has an unreachable arm. |
| HEM3003 | A JSX expression or attribute form is outside Hem’s supported lowering subset. |
| HEM2016 | A JSX match pattern has an ambiguous binding. |
Normative source
Specification 0.2, §6–§7 (expressions and control flow), §11 (JSX), and grammar 0.2 Appendix A.11. The tour’s JSX overview is section 12.