Skip to main content
Stan.js is written in TypeScript and provides excellent type inference, ensuring type safety throughout your application with minimal type annotations.

Automatic Type Inference

Stan.js infers types from your initial state:
import { createStore } from 'stan-js'

const { useStore, actions, getState } = createStore({
  counter: 0,
  message: 'hello',
  users: [] as Array<string>
})

// TypeScript knows the exact types
const state = getState()
//    ^? { counter: number, message: string, users: string[] }

actions.setCounter(5) // ✓
actions.setCounter('invalid') // ✗ Error: Argument of type 'string' is not assignable to parameter of type 'number'

Action Type Safety

Setter functions are fully typed:
const { setCounter, setMessage } = useStore()

// Direct value assignment
setCounter(10) // ✓
setCounter('10') // ✗ Type error

// Functional updates
setCounter(prev => prev + 1) // ✓
setCounter(prev => prev + '1') // ✗ Type error: can't return string

// Arrays
const { setUsers } = useStore()
setUsers([]) // ✓
setUsers(['Alice', 'Bob']) // ✓
setUsers(['Alice', 123]) // ✗ Type error
setUsers(prev => [...prev, 'Charlie']) // ✓

Explicit Type Annotations

Use as or generics for complex types:
interface User {
  id: string
  name: string
  email: string
}

interface TodoItem {
  id: string
  text: string
  completed: boolean
}

const { useStore } = createStore({
  user: null as User | null,
  todos: [] as Array<TodoItem>,
  preferences: {} as Record<string, unknown>
})
Always annotate empty arrays and objects with their intended types to get proper type inference.

Computed Values

Computed values are type-safe and inferred:
const { useStore } = createStore({
  firstName: 'John',
  lastName: 'Doe',
  age: 30,
  get fullName() {
    return `${this.firstName} ${this.lastName}`
    //       ^? string (inferred)
  },
  get isAdult() {
    return this.age >= 18
    //       ^? boolean (inferred)
  }
})

const { fullName, isAdult } = useStore()
//      ^? string
//                ^? boolean

Custom Actions with Types

Type custom actions for full safety:
interface User {
  id: string
  name: string
  email: string
}

const { useStore } = createStore(
  {
    user: null as User | null,
    isLoading: false,
    error: null as string | null
  },
  ({ actions }) => ({
    fetchUser: async (userId: string) => {
      actions.setIsLoading(true)
      actions.setError(null)
      
      try {
        const response = await fetch(`/api/users/${userId}`)
        const user: User = await response.json()
        actions.setUser(user)
      } catch (err) {
        actions.setError(err instanceof Error ? err.message : 'Unknown error')
      } finally {
        actions.setIsLoading(false)
      }
    },
    clearUser: () => {
      actions.setUser(null)
    }
  })
)

// Fully typed
const { fetchUser, clearUser } = useStore()
fetchUser('123') // ✓
fetchUser(123) // ✗ Error: Argument of type 'number' is not assignable to parameter of type 'string'

Readonly Properties

Use readonly to prevent accidental mutations:
const { useStore } = createStore({
  config: {
    apiUrl: 'https://api.example.com',
    timeout: 5000
  } as const,
  readonly appVersion: '1.0.0'
})

const { setConfig, setAppVersion } = useStore()
//                 ^? Property 'setAppVersion' does not exist

// config is still mutable
setConfig({ apiUrl: 'https://api2.example.com', timeout: 3000 }) // ✓
Readonly properties don’t generate setter functions. Use getters for computed readonly values.

Storage with Types

Type storage synchronizers:
import { storage } from 'stan-js/storage'

interface UserPreferences {
  theme: 'light' | 'dark'
  language: string
  notifications: boolean
}

const { useStore } = createStore({
  preferences: storage<UserPreferences>({
    theme: 'light',
    language: 'en',
    notifications: true
  })
})

const { preferences, setPreferences } = useStore()
//      ^? UserPreferences

setPreferences({ theme: 'dark', language: 'en', notifications: false }) // ✓
setPreferences({ theme: 'blue', language: 'en', notifications: false }) // ✗ Error: 'blue' is not assignable to 'light' | 'dark'

Custom Serialization Types

import { storage } from 'stan-js/storage'

class CustomDate {
  constructor(public date: Date) {}
  
  toJSON() {
    return this.date.toISOString()
  }
}

const { useStore } = createStore({
  createdAt: storage(new CustomDate(new Date()), {
    serialize: (value) => value.toJSON(),
    deserialize: (str) => new CustomDate(new Date(str))
  })
})

const { createdAt } = useStore()
//      ^? CustomDate

Scoped Store Types

Scoped stores maintain full type safety:
import { createScopedStore } from 'stan-js'

interface Todo {
  id: string
  text: string
  completed: boolean
}

const { StoreProvider, useStore, useScopedStore } = createScopedStore({
  todos: [] as Array<Todo>,
  filter: 'all' as 'all' | 'active' | 'completed'
})

const Component = () => {
  const { todos, setTodos, filter, setFilter } = useStore()
  //      ^? Todo[]
  //                            ^? 'all' | 'active' | 'completed'

  const store = useScopedStore()
  //    ^? Full store API with types

  return null
}

Vanilla Store Types

import { createStore } from 'stan-js/vanilla'

const store = createStore({
  counter: 0,
  message: 'hello'
})

const state = store.getState()
//    ^? { counter: number, message: string }

store.actions.setCounter(5) // ✓

store.effect(({ counter }) => {
  console.log(counter.toFixed(2)) // ✓ TypeScript knows counter is a number
})

Type Utilities

Stan.js provides internal type utilities you can use:

RemoveReadonly

Extract writable properties:
import { RemoveReadonly } from 'stan-js'

type State = {
  counter: number
  readonly appVersion: string
}

type WritableState = RemoveReadonly<State>
//   ^? { counter: number }

Actions Type

Generate action types:
import { Actions } from 'stan-js'

type State = {
  counter: number
  message: string
}

type StateActions = Actions<State>
//   ^? {
//        setCounter: (value: number | ((prev: number) => number)) => void
//        setMessage: (value: string | ((prev: string) => string)) => void
//      }

Generic Store Function

Create reusable store factories:
import { createStore } from 'stan-js'

function createCounterStore<T extends { count: number }>(initialState: T) {
  return createStore(initialState)
}

const store1 = createCounterStore({
  count: 0,
  label: 'Counter A'
})

const store2 = createCounterStore({
  count: 10,
  label: 'Counter B',
  multiplier: 2
})

Discriminated Unions

Use discriminated unions for state machines:
type Status =
  | { type: 'idle' }
  | { type: 'loading' }
  | { type: 'success'; data: string }
  | { type: 'error'; error: string }

const { useStore } = createStore({
  status: { type: 'idle' } as Status
})

const Component = () => {
  const { status, setStatus } = useStore()

  if (status.type === 'success') {
    console.log(status.data) // ✓ TypeScript narrows the type
  }

  if (status.type === 'error') {
    console.log(status.error) // ✓
  }

  setStatus({ type: 'success', data: 'Hello' }) // ✓
  setStatus({ type: 'success' }) // ✗ Error: Property 'data' is missing
}

Type Inference Best Practices

Annotate Empty Collections

Always type empty arrays and objects: [] as Array<User>, not just [].

Use Union Types

Define specific unions for string literals: 'light' | 'dark' instead of string.

Leverage Computed Types

Let TypeScript infer computed values from getters automatically.

Type Custom Actions

Always provide parameter types for custom actions to ensure safety.

Common Type Patterns

Optional Data

interface User {
  id: string
  name: string
}

const { useStore } = createStore({
  user: null as User | null,
  selectedId: undefined as string | undefined
})

Nested Objects

interface AppState {
  ui: {
    sidebarOpen: boolean
    theme: 'light' | 'dark'
  }
  data: {
    users: Array<User>
    posts: Array<Post>
  }
}

const { useStore } = createStore({
  ui: {
    sidebarOpen: false,
    theme: 'light'
  } as AppState['ui'],
  data: {
    users: [],
    posts: []
  } as AppState['data']
})

Generic Collections

const { useStore } = createStore({
  cache: new Map<string, unknown>(),
  queue: new Set<string>()
})

const { cache, setCache } = useStore()
cache.set('key', 'value') // ✓
setCache(new Map([['key2', 'value2']])) // ✓

Troubleshooting Types

Type Widening

Problem:
const { useStore } = createStore({
  theme: 'light' // TypeScript infers 'string', not 'light'
})
Solution:
const { useStore } = createStore({
  theme: 'light' as 'light' | 'dark'
})

Function State

Problem:
const { useStore } = createStore({
  callback: () => {} // ✗ Error: Function cannot be passed as top level state value
})
Solution: Store functions in objects or use custom actions:
const { useStore } = createStore(
  { data: null },
  () => ({
    callback: () => console.log('Action!')
  })
)

Build docs developers (and LLMs) love