Fine-grained reactivity, fully typed
SolidJS skips the virtual DOM entirely and instead updates individual DOM nodes directly through a fine-grained signal system. TypeScript accompanies this model with precise types for signal access, component props and typed control flow components.
Table of Contents
- 1. Fine-grained reactivity instead of re-rendering the component
- 2. createSignal: getter and setter as a typed tuple
- 3. createEffect and createMemo: derived and reactive computations
- 4. Declaring components and props with types
- 5. Typed control flow components: For, Show, Switch
- 6. createResource: loading async data with type safety
- 7. Stores: updating nested reactive objects with type safety
- 8. SolidJS compared to React's rendering model
- 9. Common TypeScript pitfalls in SolidJS code
- 10. Summary
- 11. FAQ
1. Fine-grained reactivity instead of re-rendering the component
The fundamental difference from React is that a SolidJS component function runs exactly once to produce the description of the DOM tree. Changes to reactive values afterward never trigger another call of the component function, but instead update precisely the DOM nodes or attributes that actually depend on that value.
This granularity has direct consequences for how TypeScript is used: a variable read from a signal must be called as a function to obtain its current value, instead of simply appearing as a variable in JSX like in React. TypeScript strictly distinguishes between the getter itself and the value contained within it.
2. createSignal: getter and setter as a typed tuple
createSignal returns a tuple of a parameterless getter of type Accessor and a setter of type Setter. The generic type parameter is either inferred from the initial value or specified explicitly for optional values, for example createSignal, so the setter later also accepts real user objects.
The setter accepts both a direct new value and an updater function that receives the previous value and returns the new one, which matters especially for object and array state to guarantee consistent updates. TypeScript checks both call forms against the same generic type.
import { createSignal } from 'solid-js';
const [count, setCount] = createSignal(0);
const [user, setUser] = createSignal<User | null>(null);
setCount((prev) => prev + 1);
setUser({ id: '1', name: 'Developer' });
console.log(count()); // call the getter to read the value
3. createEffect and createMemo: derived and reactive computations
createEffect runs a function immediately on first call and then automatically re-runs it whenever any signal value read inside it changes. SolidJS tracks these dependencies automatically simply by observing which getters were actually called inside the effect function, with no explicit dependency array like in React hooks.
createMemo computes a derived, cached value from reactive values and itself returns an Accessor. The returned value only gets recomputed when one of the read dependencies actually changes, which keeps expensive computations such as filtering large lists efficient.
import { createEffect, createMemo, createSignal } from 'solid-js';
const [items, setItems] = createSignal<string[]>(['a', 'bb', 'ccc']);
const [minLength, setMinLength] = createSignal(2);
const filtered = createMemo(() =>
items().filter((item) => item.length >= minLength()),
);
createEffect(() => {
console.log('Filtered count:', filtered().length);
});
4. Declaring components and props with types
A SolidJS component is an ordinary function whose props parameter can be typed with the helper type Component from solid-js. Because a component function only runs once, props must never be destructured, since destructuring destroys reactive access to individual fields and only freezes the value at call time.
TypeScript cannot automatically prevent this pitfall, which is why discipline matters here: instead of function Greeting({ name }: Props), write function Greeting(props: Props) and reference props.name directly in JSX, so later changes to name actually arrive reactively in the DOM.
import type { Component } from 'solid-js';
type GreetingProps = {
name: string;
onGreet?: (name: string) => void;
};
export const Greeting: Component<GreetingProps> = (props) => {
return (
<button onClick={() => props.onGreet?.(props.name)}>
Hello, {props.name}
</button>
);
};
5. Typed control flow components: For, Show, Switch
Because JSX expressions in SolidJS only evaluate once, dedicated components such as For, Show and Switch/Match replace the JavaScript constructs familiar from React, such as array.map() or ternary expressions, to correctly express reactive behavior. Each of these components is generically typed and adapts the type of its render callback to the element type of the supplied list.
Show works with a generic when prop whose truthiness TypeScript uses inside the callback for correct type narrowing: if when is of type User | null, the children callback receives the already narrowed User type on the truthy branch, with no additional manual check required.
import { For, Show } from 'solid-js';
function UserList(props: { users: User[]; selected: User | null }) {
return (
<>
<Show when={props.selected} fallback={<p>No selection</p>}>
{(selected) => <p>Selected: {selected().name}</p>}
</Show>
<For each={props.users}>
{(user) => <li>{user.name}</li>}
</For>
</>
);
}
6. createResource: loading async data with type safety
createResource encapsulates loading asynchronous data and returns an accessor that carries additional reactive fields such as loading and error alongside the actual value. The generic type parameter of the fetcher function automatically determines the type of the returned value, so data() is either undefined while loading or the fully typed value once loading succeeds.
When the fetcher is combined with a reactive source as its first argument, for example a user ID from a signal, every change to that source automatically triggers a new, correctly typed fetch, with no need to write a manual useEffect style replacement with an explicit dependency array.
import { createResource, createSignal } from 'solid-js';
const [userId, setUserId] = createSignal('u1');
async function fetchUser(id: string): Promise<User> {
const res = await fetch(`/api/users/${id}`);
return res.json();
}
const [user] = createResource(userId, fetchUser);
// user() is User | undefined, user.loading is boolean
7. Stores: updating nested reactive objects with type safety
For more complex, nested state, createStore from solid-js/store offers a fine-grained reactive alternative to several individual signals. Unlike createSignal, the store getter returns a reactive proxy object whose individual fields can be read reactively and independently, without accessing one field extending reactivity to others.
Updates happen through a typed setter that supports both path arguments and updater functions, for example setStore('user', 'name', 'New name'). TypeScript checks that the supplied path actually matches the nested structure of the store type, which is far less error prone with deeply nested state than manual immutable update spreading.
import { createStore } from 'solid-js/store';
type AppState = {
user: { id: string; name: string };
settings: { darkMode: boolean };
};
const [store, setStore] = createStore<AppState>({
user: { id: '1', name: 'Developer' },
settings: { darkMode: false },
});
setStore('settings', 'darkMode', (dark) => !dark);
8. SolidJS compared to React's rendering model
In React, every state change triggers another call of the component function, followed by a reconciliation pass through the virtual DOM to figure out which real DOM nodes actually need updating. SolidJS skips this comparison step entirely, since its compilation system already knows at build time which DOM node depends on which signal, and generates direct update functions for it.
From a TypeScript perspective, that means hooks rules like React's fixed call order for useState are irrelevant for SolidJS, since createSignal and friends are not hooks in the React sense, but ordinary functions that can also be called inside conditionals or loops without risking any rule violations.
9. Common TypeScript pitfalls in SolidJS code
The most common mistake is accidentally using a signal as a value instead of a function, for example if (count > 5) instead of if (count() > 5). TypeScript reliably catches this mistake, since comparing a function to a number produces a type error, which in practice catches many of these bugs already at compile time before they show up at runtime as missing reactivity tracking.
A subtler pitfall concerns asynchronous callbacks inside createEffect: if a signal getter is only called after an await, SolidJS no longer recognizes that dependency, since automatic dependency tracking only works synchronously during the first run of the effect function. TypeScript does not warn about this automatically, which is why signal access should deliberately be placed before the first await.
| Aspect | SolidJS | React | Advantage |
|---|---|---|---|
| Update mechanism | Direct, fine-grained DOM updates | Re-render with virtual DOM diffing | No diffing overhead in SolidJS |
| Signal access | count() as a function call | count as a plain variable | TypeScript enforces correct access |
| Props handling | Never destructure | Destructuring is common | Preserves reactivity in SolidJS |
| Dependency array | Tracked automatically | Manual useEffect array | Fewer sources of error in SolidJS |
Mironsoft
TypeScript migration, type safety, and team onboarding
A JavaScript codebase without type safety, but no time for a full migration?
We migrate existing JavaScript projects to TypeScript step by step, set up strict compiler settings cleanly, and bring teams to the same type-safety level with code reviews and style guides.
Migration Roadmap
Plan and execute a gradual JS-to-TS migration without big-bang risk.
Strict Mode Rollout
Set up tsconfig.json, ESLint rules, and CI checks for lasting type safety.
Team Onboarding
Bring developers up to speed on TypeScript best practices with workshops and reviews.
10. Summary
TypeScript with SolidJS
Signal type
createSignal
Components
Component
Control flow
For, Show, Switch replace map() and ternary expressions
Async data
createResource returns a typed value plus loading/error