21. TypeScript в Solid.js
🔷 TypeScript в Solid.js
Заголовок раздела «🔷 TypeScript в Solid.js»Привет! 👋 Solid.js и TypeScript — это идеальная пара. Solid написан на TypeScript, и все его API имеют отличные типы “из коробки”. Никаких @types/solid-js — всё уже включено!
Думай о TypeScript в Solid как о страховой сетке: пока ты жонглируешь сигналами и стористами, TypeScript ловит тебя, когда ты пытаешься прочитать сигнал как число, хотя там строка.
📦 Типизация сигналов: Signal<T>, Accessor<T>, Setter<T>
Заголовок раздела «📦 Типизация сигналов: Signal<T>, Accessor<T>, Setter<T>»Это три кита типизации сигналов в 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 и Setter?
Заголовок раздела «Зачем разделять Accessor и Setter?»// Можно передавать только право чтения (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} /> </> );}🧠 Типизация createMemo и createEffect
Заголовок раздела «🧠 Типизация createMemo и createEffect»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;});🏗️ Типизация пропсов: Component<Props> и ParentProps
Заголовок раздела «🏗️ Типизация пропсов: Component<Props> и ParentProps»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}🗂️ Типизация createStore
Заголовок раздела «🗂️ Типизация createStore»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);}🌐 Типизация Context API
Заголовок раздела «🌐 Типизация Context API»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}/>
// Дженерик-компонент для Selectinterface 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>; });