SolidJS 如何与 TypeScript 配合使用?
SolidJS 内置了完善的 TypeScript 支持,从项目初始化到日常开发,类型系统贯穿始终。本文围绕实际开发中最常遇到类型问题的场景,逐一拆解 SolidJS 的类型定义方法与最佳实践。
项目配置:让 TypeScript 正确识别 Solid
SolidJS 使用自己的 JSX 转换,和 React 不同,必须正确配置 tsconfig.json,否则类型检查和编译都会出问题:
json{ "compilerOptions": { "jsx": "preserve", "jsxImportSource": "solid-js", "noImplicitAny": true, "strict": true, "target": "ESNext", "moduleResolution": "node" } }
关键点在于 "jsx": "preserve" 不能改成 "react",因为 Solid 自带 JSX 转换插件处理模板。如果你用 Vite 构建,还需要 vite-plugin-solid 插件配合。
在混合 React 和 Solid 的项目中,可以在单个文件顶部加上 /** @jsxImportSource solid-js */,让 TypeScript 识别该文件使用 Solid 的 JSX 类型。
Signal 类型:从基础到进阶
Signal 是 SolidJS 的核心原语,类型定义的好坏直接影响开发体验。
基本用法
typescriptimport { createSignal } from 'solid-js'; // 显式指定泛型 const [count, setCount] = createSignal<number>(0); const [name, setName] = createSignal<string>(''); // 可空类型:signal 没有初始值时必须处理 undefined const [user, setUser] = createSignal<User | undefined>(); // user() 的类型是 User | undefined,每次使用都需要判空
Accessor 与 Setter 类型
当你需要在函数之间传递 signal 的读取端或写入端时,使用 SolidJS 提供的工具类型:
typescriptimport type { Accessor, Setter } from 'solid-js'; function useCounter(initial: number): [Accessor<number>, Setter<number>] { const [count, setCount] = createSignal(initial); return [count, setCount]; }
Accessor<T> 本质上就是 () => T,但语义更明确。Setter<T> 比较复杂,它同时接受直接值和回调函数两种形式:
typescript// Setter 的两种调用方式 setCount(5); // 直接赋值 setCount(prev => prev + 1); // 基于前值计算
注意:如果 setCount(value) 报类型错误,通常是因为 value 的类型同时满足值和函数签名,TypeScript 无法区分。这时用 setCount(() => value) 包一层即可。
派生 Signal 的类型推断
typescriptconst [firstName, setFirstName] = createSignal('Zhang'); const [lastName, setLastName] = createSignal('San'); // 派生 signal 自动推断类型,无需手动标注 const fullName = () => `${firstName()} ${lastName()}`; // fullName 的类型自动推断为 Accessor<string>
组件与 Props 类型定义
函数组件的两种写法
typescriptimport type { Component, JSX } from 'solid-js'; // 方式一:使用 Component 工具类型 interface ButtonProps { label: string; variant?: 'primary' | 'secondary'; onClick: () => void; } const Button: Component<ButtonProps> = (props) => { return <button onClick={props.onClick}>{props.label}</button>; }; // 方式二:直接标注 props 参数(更灵活,支持 children 类型控制) function Button2(props: ButtonProps): JSX.Element { return <button onClick={props.onClick}>{props.label}</button>; }
推荐方式二,因为 Component 类型默认把 children 设为可选,而直接标注可以精确控制 children 是否必须。
Props 相关工具类型
typescriptimport type { ParentProps, FlowProps, MergeProps } from 'solid-js'; // ParentProps:包含 children 的 props interface CardProps { title: string; } const Card = (props: ParentProps<CardProps>) => { return ( <div> <h2>{props.title}</h2> {props.children} </div> ); }; // FlowProps:用于 <Show>、<For> 等控制流组件 interface ListProps<T> { each: T[]; fallback?: JSX.Element; } function List<T>(props: FlowProps<ListProps<T>, T>) { // FlowProps 第二个泛型参数是 children 的参数类型 } // mergeProps:类型安全的默认 props 合并 import { mergeProps } from 'solid-js'; const defaultProps: Required<ButtonProps> = { label: '', variant: 'primary', onClick: () => {}, }; function Button(props: ButtonProps) { const merged = mergeProps(defaultProps, props); // merged 的类型是 Required<ButtonProps>,所有字段都有值 }
Props 的解构陷阱
SolidJS 中绝不能解构 props,否则会丢失响应性。类型系统可以帮助你避免这个问题:
typescript// 错误:解构后丢失响应性 function Bad({ label, onClick }: ButtonProps) { ... } // 正确:通过 props 对象访问 function Good(props: ButtonProps) { return <button onClick={props.onClick}>{props.label}</button>; }
Store 类型:深层响应式的类型安全
typescriptimport { createStore } from 'solid-js/store'; interface AppState { user: { name: string; age: number; preferences: { theme: 'light' | 'dark'; language: string; }; }; items: Array<{ id: number; name: string; completed: boolean; }>; } const [state, setState] = createStore<AppState>({ user: { name: '', age: 0, preferences: { theme: 'light', language: 'zh' }, }, items: [], }); // 类型安全的嵌套更新 setState('user', 'name', 'Zhang San'); // 正确 setState('user', 'preferences', 'theme', 'dark'); // 正确 setState('user', 'invalid', 'value'); // 编译报错 // 数组项的类型安全更新 setState('items', 0, 'completed', true); // 正确
Store 的类型定义关键是确保嵌套结构和 setState 的路径参数一一对应。TypeScript 会在编译期拦截非法路径,这正是 Store 相比普通对象的优势。
Resource 类型:异步数据加载
typescriptimport { createResource } from 'solid-js'; interface User { id: number; name: string; email: string; } async function fetchUser(id: number): Promise<User> { const res = await fetch(`/api/users/${id}`); return res.json(); } // 基本用法 const [user] = createResource<User>(() => fetchUser(1)); // user() 类型为 User | undefined,loading 时为 undefined // 带错误类型的完整定义 const [users, { refetch }] = createResource<User[], Error>(fetchUsers, { initialValue: [], }); // 在组件中使用 function UserProfile() { const [user] = createResource<User>(() => fetchUser(1)); return ( <Switch fallback={<p>加载中...</p>}> <Match when={user.error}> <p>加载失败:{user.error.message}</p> </Match> <Match when={user()}> {(u) => <p>{u().name}</p>} </Match> </Switch> ); }
<Match when={user()}> 中的回调参数 u 已经被正确收窄为 User 类型,不再是 User | undefined,这是 SolidJS 类型系统的一大亮点。
Context 类型:跨组件通信
typescriptimport { createContext, useContext } from 'solid-js'; import type { Context } from 'solid-js'; interface ThemeContextType { theme: Accessor<'light' | 'dark'>; toggleTheme: () => void; } // 创建带默认值的 context const ThemeContext = createContext<ThemeContextType>(); // 也可以创建无默认值的 context,使用时必须判空 const AuthContext = createContext<AuthContextType>(); // 类型安全的 useHook function useTheme(): ThemeContextType { const ctx = useContext(ThemeContext); if (!ctx) { throw new Error('useTheme must be used within ThemeProvider'); } return ctx; }
把 useContext 封装为自定义 hook 并加上判空保护,是避免运行时 undefined 错误的标准做法。
自定义 JSX 元素和事件类型
SolidJS 允许扩展 JSX 命名空间来支持自定义元素和事件:
typescriptdeclare module 'solid-js' { namespace JSX { // 自定义原生元素 interface IntrinsicElements { 'my-custom-element': { value?: string; onChange?: (value: string) => void; }; } // 自定义事件(用于 on:xxx 语法) interface CustomEvents { 'my-event': CustomEvent<{ detail: string }>; } // 自定义捕获事件 interface CustomCaptureEvents { 'my-capture-event': CustomEvent; } // 强制属性(用于 prop:xxx 语法) interface ExplicitProperties { 'my-prop': string; } // 自定义属性(用于 attr:xxx 语法) interface ExplicitAttributes { 'my-attr': string; } } }
这些扩展让你在使用 Web Components 或自定义 DOM 元素时也能获得完整的类型提示。
常见类型问题与排错
1. JSX 元素类型不兼容
出现 Type 'Element' is not assignable to type 'Element' 这类错误,通常是项目中同时安装了 React 的类型定义,导致 JSX 命名空间冲突。解决方法是在 tsconfig.json 中确保 jsxImportSource 只指向 solid-js,或排除 @types/react。
2. Signal 间接传递后类型丢失
typescript// 错误:传递 signal 调用结果而非 signal 本身 const count = count(); // 丢失响应性,类型变为 number // 正确:传递 Accessor const countAccessor: Accessor<number> = count;
3. 组件 children 类型
typescript// 如果组件不接受 children,props 不要用 ParentProps interface NoChildProps { title: string; } // 正确:普通接口,没有 children const Header = (props: NoChildProps) => <h1>{props.title}</h1>;
最佳实践总结
- 始终开启 strict 模式,让 TypeScript 帮你捕获更多问题
- 用
import type导入纯类型,避免类型定义进入运行时代码 - 用函数签名而非 Component 类型定义组件,精确控制 children 类型
- Signal 显式标注泛型,可空值用
T | undefined - 封装 useContext 为自定义 hook,统一判空逻辑
- 绝对不要解构 props,使用
mergeProps处理默认值 - Store 类型与 setState 路径对齐,利用编译期检查防止非法路径
- 混合框架项目用文件级 pragma,
/** @jsxImportSource solid-js */ - 扩展 JSX 命名空间,让自定义元素和事件也有类型提示
- 遇到 Setter 类型冲突时,用回调形式
setX(() => value)替代直接赋值