TypeScript
Edit this pageTypeScript is a superset of JavaScript that enhances code reliability and predictability through the introduction of static types. While JavaScript code can be directly used in TypeScript, the added type annotations in TypeScript provide clearer code structure and documentation, making it more accessible for developers.
By leveraging standard JSX, a syntax extension to JavaScript, Solid facilitates seamless TypeScript interpretation. Moreover, Solid has built-in types for the API that heighten accuracy.
For developers eager to get started, we offer TypeScript templates on GitHub.
Configuring TypeScript
When integrating TypeScript with the Solid JSX compiler, there are some settings to make for a seamless interaction:
"jsx": "preserve"
in thetsconfig.json
retains the original JSX form. This is because Solid's JSX transformation is incompatible with TypeScript's JSX transformation."jsxImportSource": "solid-js"
designates Solid as the source of JSX types.
For a basic setup, your tsconfig.json
should resemble:
For projects with diverse JSX sources, such as a blend of React and Solid, some flexibility exists.
While it's possible to set a default jsxImportSource
in the tsconfig.json
, which will correspond with the majority of your files, TypeScript also allows file-level overrides.
Using specific pragmas within .tsx
files facilitates this:
or, if using React:
Opting for the React JSX pragma means having React and its associated dependencies fully integrated into the project. Additionally, it makes sure the project's architecture is primed for React JSX file handling, which is vital.
Migrating from JavaScript to TypeScript
Transitioning from JavaScript to TypeScript in a Solid project offers the benefits of static typing. To migrate to Typescript:
- Install TypeScript into your project.
- Run the following command to generate a
tsconfig.json
file.
- Update the contents of the
tsconfig.json
to match Solid's configuration:
- Create a TypeScript or
.tsx
file to test the setup.
If using an existing JavaScript component, import the TypeScript component:
If you wish to change the entry point file from index.jsx
to index.tsx
, you need to modify the src
attribute in <script>
to look like the following:
API types
Solid is written in TypeScript, meaning everything is typed out of the box.
The Reference Tab in the sidebar provides the API Documentation that details the types of API calls. In addition, there are several helpful definitions to make it easier for specifying explicit types.
Signals
Using createSignal<T>
, a signal's type can be defined as T
.
Here, createSignal
has the return type Signal<number | undefined>
, which corresponds to the type passed into it, and undefined
as it was uninitialized.
This resolves to a getter-setter tuple, both of which are generically typed:
In Solid, a signal's getter, like count
, is essentially a function that returns a specific type.
In this case, the type is Accessor<number | undefined>
, which translates to a function () => number | undefined
.
Since the signal was not initialized, its initial state is undefined
, therefore undefined
is included in its type.
The corresponding setter, setCount
, has a more complex type:
Essentially, this type means that the function can accept either a direct number or another function as its input.
If provided with a function, that function can take the signal's previous value as its parameter and return a new value.
Both the initial and resulting values can be a number or undefined
. Importantly, calling setCount
without any arguments will reset the signal's value to undefined
.
When using the function form of the setter, the signal's current value will always be passed to the callback as the sole argument. Additionally, the return type of the setter will align with the type of value passed into it, echoing the behavior expected from a typical assignment operation.
If a signal is intended to store functions, the setter won't directly accept new functions as values. This is because it can not distinguish whether the function should be executed to yield the actual value or to store it as-is. In these situations, using the callback form of the setter is recommended:
Default values
By providing default values when createSignal
is called, the need for explicit type specification is avoided and the possibility of the | undefined
type is eliminated.
This leverages type inference to determine the type automatically:
In this example, TypeScript understands the types as number
and string
.
This means that count
and name
directly receive the types Accessor<number>
and Accessor<string>
, respectively, without the | undefined
tag.
Context
Just as signals use createSignal<T>
, context uses createContext<T>
, which is parameterized by the type T
of the context's value:
When invoking useContext(dataContext)
, the type contained within the context is returned.
For example, if the context is Context<Data | undefined>
, then with using useContext
a result of type Data | undefined
will return.
The | undefined
signifies that the context may not be defined in the component's ancestor hierarchy.
dataContext
will be understood as Context<Data | undefined>
by Solid.
Calling useContext(dataContext)
mirrors this type, returning Data | undefined
.
The | undefined
arises when the context's value will be used but cannot be determined.
Much like default values in signals, | undefined
can be avoided in the type by giving a default value that will be returned if no value is assigned by a context provider:
By providing a default value, TypeScript determines that dataContext
is Context<{ count: number, name: string }>
.
This is equivalent to Context<Data>
but without | undefined
.
A common approach to this is forming a factory function to generate a context's value.
By using TypeScript's ReturnType
, you can use the return type of this function to type this context:
CountNameContextType
will correspond to the result of makeCountNameContext
:
To retrieve the context, use useCountNameContext
, which has a type signature of () => CountNameContextType | undefined
.
In scenarios where undefined
needs to be avoided as a possible type, assert that the context will always be present.
Additionally, throwing a readable error may be preferable to non-null asserting:
Note: While supplying a default value to createContext
can make the context perpetually defined, this approach may not always be advisable.
Depending on the use case, it could lead to silent failures, which may be less preferable.
Components
The basics
By default, components in Solid use the generic Component<P>
type, where P
represents the props' type that is an object.
A JSX.Element
denotes anything renderable by Solid, which could be a DOM node, array of JSX elements, or functions yielding JSX.Element
.
Trying to pass unnecessary props or children will result in type errors:
Components with props
For components that require the use of props, they can be typed using generics:
Components with children
Often, components may need to accept child elements.
For this, Solid provides ParentComponent
, which includes children?
as an optional prop.
If defining a component with the function
keyword, ParentProps
can be used as a helper for the props:
In this example, props
is inferred to be of the type {children?: JSX.Element }
, streamlining the process of defining components that can accept children.
Special component types
Solid offers subtypes for components dealing uniquely with children:
- VoidComponent: When a component should not accept children.
- FlowComponent: Designed for components like
<Show>
or<For>
, typically requiring children and, occasionally, specific children types.
These types make sure that the children fit the required type, maintaining consistent component behaviour.
Components without the Component
types
Using the Component
types is a matter of preference over a strict requirement.
Any function that takes props and returns a JSX.Element qualifies as a valid component:
It is worth noting that the Component
types cannot be used to create generic components.
Instead, the generics will have to be typed explicitly:
Each Component
type has a corresponding Props
type that defines the shape
of its properties. These Props
types also accept the same generic types as
their associated Component
types.
Event handling
Basics
In Solid, the type for event handlers is specified as JSX.EventHandler<TElement, TEvent>
.
Here, TElement
refers to the type of the element the event is linked to.
TEvent
will indicate the type of the event itself which can serve as an alternative to (event: TEvent) => void
in the code.
This approach guarantees accurate typing for currentTarget
and target
within the event object while also eliminating the need for inline event handlers.
Inline handlers
Defining an event handler inline within a JSX attribute automatically provides type inference and checking, eliminating the need for additional typing efforts:
currentTarget
and target
In the context of event delegation, the difference between currentTarget
and target
is important:
currentTarget
: Represents the DOM element to which the event handler is attached.target
: Any DOM element within the hierarchy ofcurrentTarget
that has initiated the event.
In the type signature JSX.EventHandler<T, E>
, currentTarget
will consistently have the type T
.
However, the type of target could be more generic, potentially any DOM element.
For specific events like Input
and Focus
that are directly associated with input elements, the target will have the type HTMLInputElement
.
ref
attribute
Basics
In an environment without TypeScript, using the ref
attribute in Solid ensures that the corresponding DOM element is assigned to the variable after it is rendered:
In a TypeScript environment, particularly with strict null
checks enabled, typing these variables can be a challenge.
A safe approach in TypeScript is to acknowledge that divRef
may initially be undefined
and to implement checks when accessing it:
Within the scope of the onMount
function, which runs after rendering, you can use a non-null
assertion (indicated by the exclamation mark !
):
Another approach is to bypass null
during the assignment phase and then apply a definite assignment assertion within the ref
attribute:
In this case, using divRef!
within the ref
attribute signals to TypeScript that divRef
will receive an assignment after this stage, which is more in line with how Solid works.
While TypeScript does catch incorrect usage of refs that occur before their
JSX block definition, it currently does not identify undefined variables
within certain nested functions in Solid. Therefore, additional care is needed
when using ref
s in functions such as
createMemo
,
createRenderEffect
,
and createComputed
.
Finally, a riskier approach involves using the definite assignment assertion right at the point of variable initialization. While this method bypasses TypeScript's assignment checks for that particular variable, it offers a quick but less secure workaround that could lead to runtime errors.
Control flow-based narrowing
Control flow-based narrowing involves refining the type of a value by using control flow statements.
Consider the following:
In Solid, however, accessors cannot be narrowed in a similar way:
In this case, using optional chaining serves as an good alternative:
This approach is similar using the keyed option, but offers an accessor to prevent the recreation of children each time the when
value changes.
Note that optional chaining may not always be possible.
For instance, when a UserPanel
component exclusively requires a User
object:
If possible, consider refactoring UserPanel
to accept undefined
.
This minimizes the changes required when user
goes from undefined
to User
.
Otherwise, using Show's callback form works:
Casting can also be a solution so long as the assumption is valid:
It's worth noting that runtime type errors may arise from doing this.
This may happen when passing a type-cast value to a component, which discards information that may be nullish followed by accessing it asynchronously, such as in an event handler or timeout, or in onCleanup
.
<Show>
only excludes null
, undefined
, and false
from when
when using the callback form.
If the types in a union need to be differentiated, a memo or computed signal can work as an alternative solution:
The following alternative also works when using Show
:
Advanced JSX attributes and directives
Custom event handlers
To handle custom events in Solid, you can use the attribute on:___
.
Typing these events requires an extension of Solid's JSX namespace.
It is now possible to use the intersection EventListenerObject & AddEventListenerOptions
to provide listener options as follows:
Note:
By default, using native events like mousemove
with the on
prefix — for example, <div on:mousemove={e => {}} />
— will trigger a TypeScript error.
This occurs because these native events are not part of Solid's custom event type definitions.
To solve this, the CustomEvents
interface can be extended to include events from the HTMLElementEventMap
:
To include all native events:
To include specific native events, you can choose certain events (e.g. mousemove
and pointermove
):
Forcing properties and custom attributes
In Solid, the prop:___
directive allows explicit property setting, which is useful for retaining the original data types like objects or arrays.
attr:___
directive allows custom attributes, on the other hand, and it is effective for handling string-based HTML attributes.
Custom directives
In Solid, custom directives can be applied using the use:___
attribute, which usually accepts a target element and a JSX attribute value.
The traditional Directives
interface types these values directly (i.e. the type of value
in <div use:foo={value} />
).
However, the newer DirectiveFunctions
interface takes a function type and derives the valid types for elements and values from it.
There are additional considerations:
- The directive function always receives a single accessor.
For multiple arguments, the syntax
<div use:foo={[a, b]} />
is an option, and an accessor to a tuple should be accepted. - The same principle holds for boolean directives, as seen in
<div use:foo />
, and for directives with static values, like<div use:foo={false} />
. DirectiveFunctions
can accept functions that do not strictly meet the type requirements; such cases will be ignored.
In using DirectiveFunctions
, there's the ability to check both arguments (if present) by detailing the entire function type:
While the Directives
interface can limit the value type passed via JSX attribute to the directive, the DirectiveFunctions
interface ensures that both element and value align with the expected types, as shown below:
Addressing import issues with directives
If directives are imported from a separate file or module, TypeScript might mistakenly remove the import thinking it is a type.
To prevent this:
- Configure
onlyRemoveTypeImports: true
inbabel-preset-typescript
. - When using
vite-plugin-solid
, setsolidPlugin({ typescript: { onlyRemoveTypeImports: true } })
invite.config.ts
.
Careful management of export type and import type is required. Including a statement in the importing module ensures TypeScript keeps the directive's import. Tree-shaking tools usually omit this code from the final bundle.