Typed Models
CxJS uses typed models to provide type-safe access to data in the store. Instead of using string paths like "user.firstName", you use accessor chains like m.user.firstName that are checked by TypeScript.
Creating a Model Proxy
Use createModel<T>() to create a proxy object that mirrors your data structure. The proxy doesn’t hold any data — it generates binding paths that connect widgets to the store.
<div class="flex flex-col gap-4">
<div class="flex gap-2">
<TextField value={m.user.firstName} placeholder="First name" />
<TextField value={m.user.lastName} placeholder="Last name" />
</div>
<div class="p-3 bg-muted rounded text-sm">
<strong>Store content</strong>
<pre class="mt-2" text={(data) => JSON.stringify(data, null, 2)} />
</div>
</div> {}When you write m.user.firstName, CxJS creates a binding to the path "user.firstName" in the store. The TextField reads and writes to this path automatically.
Why Typed Models?
Typed models provide several benefits over string-based paths:
- Type safety — TypeScript catches typos and invalid paths at compile time
- Autocomplete — Your editor suggests available properties as you type
- Refactoring — Rename a property and all usages update automatically
- Documentation — Hover over a property to see its type
Accessor Methods
Accessor chains provide two useful methods for working with paths:
| Method | Description |
|---|---|
toString() | Returns the full string path represented by the accessor. Useful when you need to pass paths to APIs that expect strings. |
nameOf() | Returns only the last segment of the path (the property name). |
<table class="w-full text-sm">
<thead>
<tr>
<th class="text-left py-2 pr-4 border-b border-border">Accessor</th>
<th class="text-left py-2 border-b border-border">Result</th>
</tr>
</thead>
<tbody>
<tr>
<td class="py-2 pr-4">
<code class="text-primary">m.user.firstName.toString()</code>
</td>
<td class="py-2">
<code>"{m.user.firstName.toString()}"</code>
</td>
</tr>
<tr>
<td class="py-2 pr-4">
<code class="text-primary">m.user.email.toString()</code>
</td>
<td class="py-2">
<code>"{m.user.email.toString()}"</code>
</td>
</tr>
<tr>
<td class="py-2 pr-4">
<code class="text-primary">m.count.toString()</code>
</td>
<td class="py-2">
<code>"{m.count.toString()}"</code>
</td>
</tr>
<tr>
<td class="py-2 pr-4">
<code class="text-primary">m.user.lastName.nameOf()</code>
</td>
<td class="py-2">
<code>"{m.user.lastName.nameOf()}"</code>
</td>
</tr>
<tr>
<td class="py-2 pr-4">
<code class="text-primary">m.count.nameOf()</code>
</td>
<td class="py-2">
<code>"{m.count.nameOf()}"</code>
</td>
</tr>
</tbody>
</table> | Accessor | Result |
|---|---|
m.user.firstName.toString() | "user.firstName" |
m.user.email.toString() | "user.email" |
m.count.toString() | "count" |
m.user.lastName.nameOf() | "lastName" |
m.count.nameOf() | "count" |
Nested Structures
Accessor chains work with deeply nested structures. Define your interfaces to match your data shape:
interface Address {
street: string;
city: string;
country: string;
}
interface User {
name: string;
address: Address;
}
interface PageModel {
user: User;
}
const m = createModel<PageModel>();
// Access nested properties
m.user.address.city; // binds to "user.address.city"
The proxy automatically generates the correct path regardless of nesting depth.
createModel can also be imported from cx/ui. createAccessorModelProxy is available as an alias for backward compatibility.