Configuration

TypeScript

编辑此页面

TypeScript 是 JavaScript 的超集,它通过引入静态类型增强了代码的可靠性和可预测性。虽然 JavaScript 代码可以直接在 TypeScript 中使用,但 TypeScript 中添加的类型注释提供了更清晰的代码结构和文档,使开发人员更容易理解。

通过借助标准的 JSX(JavaScript 的语法扩展),Solid 实现了对 TypeScript 的无缝解释。Solid 还为 API 内置了类型,以提高准确性。

对于急于开始的开发者,我们在 GitHub 上提供了 TypeScript 模板


配置 TypeScript

在将 TypeScript 与 Solid JSX 编译器集成时,需要进行一些设置以实现无缝交互:

  1. tsconfig.json 中的 "jsx": "preserve" 保持原始的 JSX 形式。这是因为 Solid 的 JSX 转换与 TypeScript 的 JSX 转换不兼容。
  2. "jsxImportSource": "solid-js" 将 Solid 指定为 JSX 类型的来源。

对于基本设置,您的 tsconfig.json 应该类似于:

{
"compilerOptions": {
"jsx": "preserve",
"jsxImportSource": "solid-js"
}
}

对于具有多种 JSX 源的项目,例如 React 和 Solid 混合使用,存在一定的灵活性。虽然可以在 tsconfig.json 中设置默认的 jsxImportSource ,这将对应于大多数文件,但 TypeScript 也允许文件级别的覆盖。在 .tsx 文件中使用特定的 pragma 可以实现这一点:

/** @jsxImportSource solid-js */

如果使用 React:

/** @jsxImportSource react */

选择 React JSX pragma 意味着将 React 及其相关依赖完全集成到项目中。此外,它还确保项目架构为处理 React JSX 文件做好准备,这一点至关重要。


从 JavaScript 迁移到 TypeScript

从 JavaScript 过渡到 TypeScript ,可以提供静态类型的好处。如果要迁移到 Typescript:

  1. 将 TypeScript 安装到您的项目中。
  1. 运行以下命令生成 tsconfig.json 文件。
  1. 更新 tsconfig.json 的内容以匹配 Solid 的配置:
{
"compilerOptions": {
"strict": true,
"target": "ESNext",
"module": "ESNext",
"moduleResolution": "node",
"allowSyntheticDefaultImports": true,
"esModuleInterop": true,
"jsx": "preserve",
"jsxImportSource": "solid-js",
"types": ["vite/client"],
"noEmit": true,
"isolatedModules": true
}
}
  1. 创建 TypeScript 或 .tsx 文件测试设置。
import { type Component } from "solid-js";
const MyTsComponent(): Component = () => {
return (
<div>
<h1>This is a TypeScript component</h1>
</div>
);
}
export default MyTsComponent;

如果使用现有的 JavaScript 组件,请导入 TypeScript 组件:

import MyTsComponent from "./MyTsComponent";
function MyJsComponent() {
return (
<>
{/* ... */}
<MyTsComponent />
</>
);
}

API 类型

Solid 是用 TypeScript 编写的,这意味着所有内容都是开箱即用的。

侧边栏中的“参考”选项卡提供了 API 调用类型的详细介绍。此外,还有几个有用的定义可以更轻松地声明显式类型。

Signals

使用 createSignal<T>,signal 的类型可以定义为 T

const [count, setCount] = createSignal<number>();

此时 createSignal 具有返回类型 Signal<number | undefined> ,它对应于传入的类型,以及 undefined(因为它未初始化)。返回的是一个 getter-setter 元组,两者都是泛型类型:

import type { Signal, Accessor, Setter } from "solid-js";
type Signal<T> = [get: Accessor<T>, set: Setter<T>];

在 Solid 中,signal 的 getter,如 count ,本质上是一个返回特定类型的函数。在本例中,类型为 Accessor<number | undefined> ,它转换为函数 () => number | undefined 。由于 signal 未初始化,其初始状态为 undefined ,因此 undefined 包含在其类型中。

对应的 setter setCount 具有更复杂的类型:

Setter<number | undefined>.

本质上,这种类型意味着该函数可以接受一个直接的数字或另一个函数作为其输入。如果提供了一个函数,该函数可以将 signal 之前的值作为其参数并返回一个新值。初始值和结果值都可以是数字或 undefined 。重要的是,调用不带任何参数的 setCount 会将 signal 的值重置为 undefined

当使用 setter 的函数形式时,signal 的当前值将始终作为唯一参数传递给回调。此外,setter 的返回类型将与传递给它的值的类型保持一致,这与典型的赋值操作的预期行为相呼应。

如果 signal 旨在存储函数,setter 不会直接接受新函数作为值。这是因为它无法区分是否应该执行该函数以产生实际值或按原样存储它。在这些情况下,建议使用 setter 的回调形式:

setSignal(() => value);

默认值

通过在调用 createSignal 时提供默认值,可以避免显式类型指定的需要,并消除 | undefined 类型的可能性。这是利用类型推断来自动确定类型:

const [count, setCount] = createSignal(0);
const [name, setName] = createSignal("");

在此示例中,TypeScript 将类型理解为 numberstring 。这意味着 countname 分别直接接收类型 Accessor<number>Accessor<string> ,而无需 | undefined 标记。

Context

正如 signal 使用 createSignal<T> 一样,上下文使用 createContext<T> ,将上下文值的类型 T 作为参数:

type Data = { count: number; name: string };

调用 useContext(dataContext) 时,将返回上下文中包含的类型。例如,如果上下文是 Context<Data | undefined> ,则使用 useContext 时将返回 Data | undefined 类型。 | undefined 表示上下文可能在组件的祖先层级结构中未定义。

dataContext 将被 Solid 理解为 Context<Data | undefined>。调用 useContext(dataContext) 反映了这个类型,返回 Data | undefined 。当上下文的值将被使用但无法确定时,就会出现| undefined

与 signal 中的默认值非常相似,可以通过给出默认值来避免类型中的 | undefined ,如果上下文 provider 未分配任何值,则将返回该默认值:

const dataContext = createContext({ count: 0, name: "" });

通过提供默认值,TypeScript 确定 dataContextContext<{ count: number, name: string }> 。这等同于 Context<Data> 但不包含 | undefined

一种常见的方法是用一个工厂函数来生成上下文的值。通过使用 TypeScript 的 ReturnType ,您可以使用此函数的返回类型来为上下文指定类型:

export const makeCountNameContext = (initialCount = 0, initialName = "") => {
const [count, setCount] = createSignal(initialCount);
const [name, setName] = createSignal(initialName);
return [
{ count, name },
{ setCount, setName },
] as const;
};
type CountNameContextType = ReturnType<typeof makeCountNameContext>;
export const CountNameContext = createContext<CountNameContextType>();

CountNameContextType will correspond to the result of makeCountNameContext:

[
{ count: Accessor<number>, name: Accessor<string> },
{ setCount: Setter<number>, setName: Setter<string> },
];

要获取上下文,使用 useCountNameContext ,它的类型签名为 () => CountNameContextType | undefined

在需要避免 undefined 作为可能类型的场景中,断言上下文将始终存在。此外,抛出可读的错误可能比非空断言更可取:

export const useCountNameContext = () => {
const countName = useContext(CountNameContext);
if (!countName) {
throw new Error(
"useCountNameContext should be called inside its ContextProvider"
);
}
return countName;
};

注意:虽然为 createContext 提供默认值可以使上下文始终保持定义状态,但这种方法可能并不总是可取的。根据具体用例,这可能导致静默失败,这可能不太可取。

组件

基础知识

默认情况下,Solid 中的组件使用泛型 Component<P> 类型,其中 P 表示 props 的类型,它是一个对象。

import type { Component } from "solid-js";
const MyComponent: Component<MyProps> = (props) => {
...
}

JSX.Element 表示 Solid 可渲染的任何内容,可以是 DOM 节点、JSX 元素数组或生成 JSX.Element 的函数。

尝试传递不必要的 props 或 children 将导致类型错误:

// in counter.tsx
const Counter: Component = () => {
const [count, setCount] = createSignal(0);
return (
<button onClick={() => setCount((prev) => prev + 1)}>{count()}</button>
);
};
// in app.tsx
<Counter />; // ✔️
<Counter initial={5} />; // ❌: No 'initial' prop defined
<Counter>hi</Counter>; // ❌: Children aren't expected

带 props 的组件

对于需要使用 props 的组件,可以使用泛型进行类型定义:

const InitCounter: Component<{ initial: number }> = (props) => {
const [count, setCount] = createSignal(props.initial);
return (
<button onClick={() => setCount((prev) => prev + 1)}>{count()}</button>
);
};
<InitCounter initial={5} />;

带 children 的组件

通常,组件可能需要接受子元素。为此,Solid 提供了 ParentComponent ,其中包括 children? 作为可选属性。如果使用 function 关键字定义组件,则 ParentProps 可以用作 props 的辅助工具:

import { ParentComponent } from "solid-js";
const CustomCounter: ParentComponent = (props) => {
const [count, setCount] = createSignal(0);
return (
<button onClick={() => setCount((prev) => prev + 1)}>
{count()}
{props.children}
</button>
);
};

在此示例中, props 被推断为 {children?: JSX.Element } 类型,简化了定义可接受子元素的组件的过程。

特殊组件类型

Solid 为专门处理子元素的组件提供了子类型:

  • VoidComponent: 当组件不应接受子组件时。
  • FlowComponent: 专为 <Show><For>之类的组件而设计,通常需要子组件,有时需要特定类型的子元素。

这些类型确保子元素符合所需类型,保持一致的组件行为。

没有 Component 类型的组件

使用 Component 类型是一个偏好问题,而不是严格的要求。任何接受 props 并返回 JSX.Element 的函数都有资格作为有效组件:

// arrow function
const MyComponent = (props: MyProps): JSX.Element => { ... }
// function declaration
function MyComponent(props: MyProps): JSX.Element { ... }
// component which takes no props
function MyComponent(): JSX.Element { ... }

值得注意的是,Component类型不能用于创建泛型组件。相反,泛型必须显式地进行类型定义:

// For arrow functions, the syntax <T> by itself is invalid in TSX because it could be confused with JSX.
const MyGenericComponent = <T extends unknown>(
props: MyProps<T>
): JSX.Element => {
/* ... */
};
// Using a function declaration for a generic component
function MyGenericComponent<T>(props: MyProps<T>): JSX.Element {
/* ... */
}

事件处理

基础知识

在 Solid 中,事件处理程序的类型指定为 JSX.EventHandler<TElement, TEvent> 。这里,TElement指的是事件链接到的元素的类型。 TEvent 表示事件本身的类型,可以在代码中替代 (event: TEvent) => void。此方法保证了事件对象中 currentTargettarget 的准确类型定义,同时还消除了对内联事件处理程序的需要。

import type { JSX } from "solid-js"
// Defining an event handler using the `EventHandler` type:
const onInput: JSX.EventHandler<HTMLInputElement, InputEvent> = (event) => {
console.log("Input changed to", event.currentTarget.value)
}
// Then attach handler to an input element:
;<input onInput={onInput} />

内联处理程序

在 JSX 属性中定义内联事件处理程序会自动提供类型推断和检查,从而无需额外的类型定义工作:

<input
onInput={(event) => {
console.log("Input changed to", event.currentTarget.value);
}}
/>

currentTargettarget

在事件委托的上下文中,currentTargettarget 之间的区别很重要:

  • currentTarget: 表示事件处理程序附加到的 DOM 元素。
  • target: currentTarget 层次结构中启动事件的任何 DOM 元素。

在类型签名 JSX.EventHandler<T, E> 中, currentTarget 将始终具有 T 类型。然而,目标的类型可以更通用,可能是任何 DOM 元素。对于与输入元素直接关联的特定事件(例如 InputFocus ),目标将具有类型 HTMLInputElement

ref 属性

基础知识

在没有 TypeScript 的环境中,在 Solid 中使用 ref 属性可以确保相应的 DOM 元素在渲染后分配给变量:

let divRef;
console.log(divRef); // Outputs: undefined
onMount(() => {
console.log(divRef); // Outputs: <div> element
});
return <div ref={divRef} />;

在 TypeScript 环境中,特别是在启用了严格的 null 检查的情况下,为这些变量添加类型可能会很有挑战性。

TypeScript 中的一种安全方法是承认 divRef 最初可能是 undefined 并在访问它时实施检查:

let divRef: HTMLDivElement | undefined
// This would be flagged as an error during compilation
divRef.focus()
onMount(() => {
if (!divRef) return
divRef.focus()
})
return <div ref={divRef}>...</div>

onMount 函数的作用域内(该函数在渲染后运行),你可以使用非 null 断言(用感叹号 ! 表示):

onMount(() => {
divRef!.focus();
});

另一种方法是在赋值阶段绕过 null,然后在 ref 属性中应用明确的赋值断言:

let divRef: HTMLDivElement
// Compilation error as expected
divRef.focus()
onMount(() => {
divRef.focus()
})
return <div ref={divRef!}>...</div>

在这种用例下,在 ref 属性中使用 divRef! 向 TypeScript 表明 divRef 将在此阶段之后接收赋值,这更符合 Solid 的工作方式。

最后,一种风险更大的方法是在变量初始化时使用明确的赋值断言。虽然此方法绕过了 TypeScript 对特定变量的赋值检查,但它提供了一个快速但安全性较低的解决方案,可能会导致运行时错误。

let divRef!: HTMLDivElement;
// Permitted by TypeScript but will throw an error at runtime:
// divRef.focus();
onMount(() => {
divRef.focus();
});

基于控制流的收窄

基于控制流的收窄是指通过使用控制流语句细化值的类型。

考虑这个例子:

const user: User | undefined = maybeUser();
return <div>{user && user.name}</div>;

然而,在 Solid 中,访问器不能以类似的方式收窄:

const [user, setUser] = createSignal<User>();
return <div>{user() && user().name}</div>;
// ^ Object may be 'undefined'.
// Using `<Show>`:
return (
<div>
<Show when={user()}>
{user().name /* Object is possibly 'undefined' */}
</Show>
</div>
);

在这种情况下,使用可选链是一个很好的替代方案:

return <div>{user()?.name}</div>;
// Using `<Show>`:
return (
<div>
<Show when={user()}>{(nonNullishUser) => nonNullishUser().name}</Show>
</div>
);

此方法类似于使用 keyed 选项,但提供了一个访问器来防止每次 when 值更改重新创建子元素。

return (
<div>
<Show keyed when={user()}>
{(nonNullishUser) => nonNullishUser.name}
</Show>
</div>
);

请注意,可选链可能并不总是可行的。例如,当 UserPanel 组件专门需要一个 User 对象时:

return <UserPanel user={user()} />;
// ^ Type 'undefined' is not assignable to type 'User'.

如果可能,请考虑重构 UserPanel 以接受 undefined 。这可以最大限度地减少 userundefined 变为 User 时所需的更改。

否则,使用 Show 的回调形式:

return (
<Show when={user()}>
{(nonNullishUser) => <UserPanel user={nonNullishUser()} />}
</Show>
);

只要假设有效,类型转换也可以是一种解决方案:

return <div>{user() && (user() as User).name}</div>;

值得注意的是,这样做可能会导致运行时类型错误。当将类型转换值传递给组件时,可能会发生这种情况,组件会丢弃可能为空的信息,然后异步访问它,例如在事件处理程序或超时中,或者在 onCleanup 中。

使用回调形式时, <Show> 仅从 when 中排除 nullundefinedfalse 。如果需要区分联合类型中的类型,可以使用 memo 或计算信号作为替代解决方案:

type User = Admin | OtherUser;
const admin = createMemo(() => {
const u = user();
return u && u.type === "admin" ? u : undefined;
});
return <Show when={admin()}>{(a) => <AdminPanel user={a()} />}</Show>;

使用 Show 时,以下替代方法也可行:

<Show
when={(() => {
const u = user();
return u && u.type === "admin" ? u : undefined;
})()}
>
{(admin) => <AdminPanel user={admin()} />}
</Show>

高级 JSX 属性和指令

自定义事件处理程序

要在 Solid 中处理自定义事件,你可以使用属性 on:___。为这些事件添加类型需要扩展 Solid 的 JSX 命名空间。

class NameEvent extends CustomEvent {
type: "Name";
detail: { name: string };
constructor(name: string) {
super("Name", { detail: { name } });
}
}
declare module "solid-js" {
namespace JSX {
interface CustomEvents {
Name: NameEvent; // Matches `on:Name`
}
}
}
// Usage
<div on:Name={(event) => console.log("name is", event.detail.name)} />;

现在可以使用交集 EventListenerObject & AddEventListenerOptions 来提供监听器选项,如下所示:

import type { JSX } from "solid-js"
const handler: JSX.EventHandlerWithOptions<HTMLDivElement, Event> = {
once: true,
handleEvent: (event) => {
console.log("will fire only once");
},
}
// Usage
<div on:click={handler} />;

强制属性和自定义属性

在 Solid 中,prop:___ 指令允许显式属性设置,这对于保留原始数据类型(如对象或数组)非常有用。另一方面,attr:___ 指令允许自定义属性,并且对于处理基于字符串的 HTML 属性非常有效。

declare module "solid-js" {
namespace JSX {
interface ExplicitProperties {
count: number;
name: string;
}
interface ExplicitAttributes {
count: number;
name: string;
}
}
}
// Usage
<Input prop:name={name()} prop:count={count()}/>
<my-web-component attr:name={name()} attr:count={count()}/>

自定义指令

在 Solid 中,可以使用 use:___ 属性实现自定义指令,该属性通常接收一个目标元素和一个 JSX 属性值。传统的 Directives 接口直接对这些值进行类型定义(即 <div use:foo={value} />value 的类型)。但是,新的 DirectiveFunctions 接口采用函数类型并从中派生元素和值的有效类型。

还有其他注意事项:

  • 指令函数始终接收单个访问器。对于多个参数,语法 <div use:foo={[a, b]} /> 是一个选项,并且应该接受元组的访问器。
  • 同样的原则也适用于布尔指令(如 <div use:foo /> 中所示)以及具有静态值的指令(如 <div use:foo={false} /> )。
  • DirectiveFunctions可以接受不严格满足类型要求的函数;此类情况将被忽略。
function model(
element: Element, // directives can be used on any HTML and SVG element
value: Accessor<Signal<string>> // second param will always be an accessor in case value being reactive
) {
const [field, setField] = value();
createRenderEffect(() => (element.value = field()));
element.addEventListener("input", (e) => {
const value = (e.target as HTMLInputElement).value;
setField(value);
});
}
declare module "solid-js" {
namespace JSX {
interface Directives {
model: Signal<string>; // Corresponds to `use:model`
}
}
}
// Usage
let [name, setName] = createSignal("");
<input type="text" use:model={[name, setName]} />;

在使用 DirectiveFunctions 时,可以通过详细说明整个函数类型来检查两个参数(如果存在):

function model(element: HTMLInputElement, value: Accessor<Signal<string>>) {
const [field, setField] = value();
createRenderEffect(() => (element.value = field()));
element.addEventListener("input", (e) => setField(e.target.value));
}
function log(element: Element) {
console.log(element);
}
let num = 0;
function count() {
num++;
}
function foo(comp: Element, args: Accessor<string[]>) {
// function body
}
declare module "solid-js" {
namespace JSX {
interface DirectiveFunctions {
model: typeof model;
log: typeof log;
count: typeof count;
foo: typeof foo;
}
}
}

虽然 Directives 接口可以限制通过 JSX 属性传递给指令的值类型,但 DirectiveFunctions 接口可以确保元素和值都符合预期类型,如下所示:

{/* This is correct */}
<input use:model={createSignal('')} />
{/* These will result in a type error */}
<input use:model />
<input use:model={7} />
<div use:model={createSignal('')} />
使用指令解决导入问题

如果指令是从单独的文件或模块导入的,TypeScript 可能会错误地认为它是一种类型而删除导入。

为了防止这种情况:

  • babel-preset-typescript 中配置 onlyRemoveTypeImports: true
  • 使用 vite-plugin-solid 时,请在 vite.config.ts 中设置 solidPlugin({ typescript: { onlyRemoveTypeImports: true } })

需要谨慎管理导出类型和导入类型。在导入模块中包含声明可确保 TypeScript 保留指令的导入。 Tree-shaking 工具通常会从最终 bundle 中省略此代码。

import { directive } from "./directives.js"
directive // prevents TypeScript's tree-shaking
<div use:directive />
报告此页面问题