Перейти к содержимому

21. TypeScript в Solid.js

Привет! 👋 Solid.js и TypeScript — это идеальная пара. Solid написан на TypeScript, и все его API имеют отличные типы “из коробки”. Никаких @types/solid-js — всё уже включено!

Думай о TypeScript в Solid как о страховой сетке: пока ты жонглируешь сигналами и стористами, TypeScript ловит тебя, когда ты пытаешься прочитать сигнал как число, хотя там строка.


Это три кита типизации сигналов в Solid:

import { createSignal, Signal, Accessor, Setter } from 'solid-js';
// createSignal возвращает кортеж [Accessor<T>, Setter<T>]
const [count, setCount] = createSignal<number>(0);
// ^Accessor<number> ^Setter<number>
// Accessor<T> — это функция () => T
// Setter<T> — это функция (value: T | ((prev: T) => T)) => T
// Можно явно типизировать через Signal<T>
const mySignal: Signal<string> = createSignal('hello');
const [value, setValue] = mySignal;
// Accessor и Setter можно использовать в пропсах
interface CounterProps {
count: Accessor<number>;
onIncrement: () => void;
}
// Дженерики в createSignal — тип выводится из начального значения
const [name, setName] = createSignal('Яша'); // Signal<string> автоматически
const [user, setUser] = createSignal<User | null>(null); // явный тип для nullable
// Можно передавать только право чтения (Accessor)
function ReadOnlyCounter({ count }: { count: Accessor<number> }) {
return <div>Счётчик: {count()}</div>;
}
// Или только право записи (Setter)
function IncrementButton({ setCount }: { setCount: Setter<number> }) {
return <button onClick={() => setCount(n => n + 1)}>+</button>;
}
// Это мощный паттерн для инкапсуляции!
function App() {
const [count, setCount] = createSignal(0);
return (
<>
<ReadOnlyCounter count={count} />
<IncrementButton setCount={setCount} />
</>
);
}

import { createMemo, createEffect, Accessor } from 'solid-js';
// createMemo возвращает Accessor<T>
const doubled: Accessor<number> = createMemo(() => count() * 2);
// С явным типом (если нужно переопределить вывод)
const formatted = createMemo<string>(() => `Значение: ${count()}`);
// createEffect не возвращает значение, типизировать нечего
createEffect(() => {
console.log('Счётчик изменился:', count());
// TypeScript знает тип count() — number
});
// createEffect с дженерик (для предыдущего значения)
createEffect<number | undefined>((prev) => {
const current = count();
if (prev !== undefined) {
console.log(`Изменение: ${prev} → ${current}`);
}
return current;
});

import { Component, ParentProps, FlowProps, JSX } from 'solid-js';
// Базовый компонент с пропсами
interface ButtonProps {
label: string;
onClick: () => void;
disabled?: boolean;
variant?: 'primary' | 'secondary' | 'danger';
}
// Component<Props> — стандартный тип для Solid-компонента
const Button: Component<ButtonProps> = (props) => {
return (
<button
onClick={props.onClick}
disabled={props.disabled}
>
{props.label}
</button>
);
};
// ParentProps — для компонентов с children
// Эквивалент: Props & { children?: JSX.Element }
const Card: Component<ParentProps<{ title: string }>> = (props) => {
return (
<div class="card">
<h3>{props.title}</h3>
<div>{props.children}</div>
</div>
);
};
// FlowProps — для контрольных компонентов (if/for/switch)
// FlowProps<P, C> = P & { children: C | C[] }
const ConditionalWrapper: Component<FlowProps<{ when: boolean }>> = (props) => {
return props.when ? <>{props.children}</> : null;
};
// JSX.Element vs JSX.JSXElement
// JSX.Element — более широкий тип (включает null, undefined, etc.)
// JSX.JSXElement — только реальные JSX-элементы
function renderContent(show: boolean): JSX.Element {
return show ? <div>Контент</div> : null; // null допустим с JSX.Element
}

import { createStore, SetStoreFunction, Store } from 'solid-js/store';
interface TodoItem {
id: number;
text: string;
completed: boolean;
}
interface AppStore {
todos: TodoItem[];
filter: 'all' | 'active' | 'completed';
loading: boolean;
}
// createStore возвращает [Store<T>, SetStoreFunction<T>]
const [store, setStore] = createStore<AppStore>({
todos: [],
filter: 'all',
loading: false,
});
// Store<T> — это readonly deep proxy
// SetStoreFunction<T> — функция для обновления
// TypeScript проверяет пути обновления!
setStore('filter', 'completed'); // ✅
setStore('todos', 0, 'completed', true); // ✅ — deep path typing
// setStore('unknownField', true); // ❌ TypeScript ошибка
// Типизация функции обновления
function updateTodo(id: number, changes: Partial<TodoItem>) {
setStore('todos', (todo) => todo.id === id, changes);
}

import { createContext, useContext, Context } from 'solid-js';
interface ThemeContextValue {
theme: Accessor<'light' | 'dark'>;
toggleTheme: () => void;
}
// createContext с типом — TypeScript знает что внутри
const ThemeContext = createContext<ThemeContextValue>();
// ThemeContext: Context<ThemeContextValue | undefined>
// Или с начальным значением (тогда undefined исключается)
const [theme, setTheme] = createSignal<'light' | 'dark'>('dark');
const ThemeContextWithDefault = createContext<ThemeContextValue>({
theme,
toggleTheme: () => setTheme(t => t === 'light' ? 'dark' : 'light'),
});
// Custom hook для типобезопасного использования контекста
function useTheme(): ThemeContextValue {
const ctx = useContext(ThemeContext);
if (!ctx) throw new Error('useTheme: нет ThemeContext!');
return ctx;
}
// В компоненте — TypeScript знает точный тип
function ThemeToggle() {
const { theme, toggleTheme } = useTheme();
return (
<button onClick={toggleTheme}>
Тема: {theme()}
</button>
);
}

import { Component, For } from 'solid-js';
// Дженерик-компонент для списка
function List<T>(props: {
items: T[];
renderItem: (item: T, index: number) => JSX.Element;
keyExtractor: (item: T) => string | number;
}) {
return (
<ul>
<For each={props.items}>
{(item, index) => (
<li key={props.keyExtractor(item)}>
{props.renderItem(item, index())}
</li>
)}
</For>
</ul>
);
}
// Использование — TypeScript выведет тип T автоматически
<List
items={[{ id: 1, name: 'Яша' }]}
renderItem={(user) => <span>{user.name}</span>} // TypeScript знает тип user!
keyExtractor={(user) => user.id}
/>
// Дженерик-компонент для Select
interface SelectProps<T> {
options: { value: T; label: string }[];
value: Accessor<T>;
onChange: (value: T) => void;
}
function Select<T extends string | number>(props: SelectProps<T>) {
return (
<select
value={String(props.value())}
onChange={(e) => props.onChange(e.target.value as T)}
>
<For each={props.options}>
{(opt) => <option value={String(opt.value)}>{opt.label}</option>}
</For>
</select>
);
}

// ❌ Ошибка: читаешь сигнал без вызова
const [count, setCount] = createSignal(0);
// const x = count + 1; // TypeScript: нельзя складывать функцию с числом!
const x = count() + 1; // ✅
// ❌ Ошибка: деструктуризация пропсов
function Bad({ name, onClick }: { name: string; onClick: () => void }) {
// Solid не отслеживает деструктурированные пропсы!
// name теряет реактивность
return <button onClick={onClick}>{name}</button>;
}
// ✅ Правильно: используй props напрямую
function Good(props: { name: string; onClick: () => void }) {
return <button onClick={props.onClick}>{props.name}</button>;
}
// ❌ Ошибка: неверное использование JSX.Element для узких типов
// Используй JSX.Element для nullable, JSX.JSXElement для non-nullable
// ✅ Правильно: явная типизация ресурсов
import { createResource, Resource } from 'solid-js';
interface User { id: number; name: string; }
const [user, { refetch }]: [Resource<User>, { refetch: () => void }] =
createResource(async () => {
const res = await fetch('/api/user');
return res.json() as Promise<User>;
});

🎯 Playground: TypeScript в Solid — интерактивная демонстрация

Заголовок раздела «🎯 Playground: TypeScript в Solid — интерактивная демонстрация»