TypeScript with SolidJS: Fine-Grained Reactivity, Fully Typed
AI generated
type
TypeScript · Frontend · Reactivity
TypeScript with SolidJS
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.

10 min read SolidJS Signals Fine-grained reactivity

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(initial) 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(null), 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 returns Accessor and Setter as a tuple

Components

Component types the props, never destructure them

Control flow

For, Show, Switch replace map() and ternary expressions

Async data

createResource returns a typed value plus loading/error

11. FAQ: TypeScript with SolidJS

1Why does a SolidJS signal have to be called as a function?
createSignal returns a getter of type Accessor, not a direct value. The function call is the mechanism through which SolidJS recognizes at runtime which DOM node depends on which signal.
2Why must props in SolidJS components never be destructured?
Destructuring reads a field's value once at call time and freezes it, so later reactive changes to that field never arrive in the DOM. Props should therefore always be referenced via props.fieldName.
3What is the difference between createEffect and createMemo?
createEffect runs a function for its side effects and returns nothing, while createMemo computes a derived, cached value from reactive values and itself returns an accessor.
4How does automatic dependency tracking work in SolidJS?
SolidJS observes which signal getters are actually called inside a reactive function like createEffect or createMemo, and automatically registers those as dependencies, with no explicit dependency array.
5Why do For and Show replace the usual JavaScript constructs?
Because JSX expressions in SolidJS only evaluate once, a raw array.map() would not update reactively when data changes. For and Show are specifically built to express reactive behavior correctly.
6What does createResource provide beyond the actual data value?
The returned accessor carries additional reactive fields such as loading and error, so the loading state of an async request can be queried in the UI with type safety, without maintaining a separate signal for it.
7When should createStore be used instead of several individual signals?
For complex, nested state where individual fields need to be read and updated reactively and independently. createStore provides a typed, fine-grained reactive proxy object for that.
8Do React's hooks rules also apply to SolidJS?
No, createSignal and related functions are not hooks in the React sense and may also be called inside conditionals or loops, since SolidJS does not rely on a fixed call order to associate state.
9Why doesn't TypeScript catch every reactivity bug in SolidJS?
TypeScript only checks types, not the runtime behavior of dependency tracking. A signal access after an await inside createEffect is type correct but is no longer captured by automatic tracking.
10How is a SolidJS component typed with Component?
Component from solid-js describes a function that accepts a props object of the given type and returns JSX. The type parameter precisely defines which fields and callback signatures the component expects.