In this post join me as I convert a simple component written with SolidJS into a TypeScript (TS) SolidJS component. We will go over setting the project’s TypeScript configuration, using the Component type, createSignals and the Accessor type and even solving that annoying style modules typing on the way.
To benefit the most from this post, you should be familiar with TypeScript fundamentals and have a basic understanding of Generics.
As you might know, I have a simple Pagination component made with SolidJS. This is a simple component which displays a set of numbered pages, which you can navigate between:
It’s currently written in plain JS and looks like this:
import {createEffect, createSignal, For, on} from 'solid-js';
import styles from './Pagination.css';
import {debugComputation} from '@solid-devtools/logger';
const NO_TOTAL_PAGES_ERROR = 'No total pages were given';
function paginationLogic(props) {
if (!props.totalPages) {
throw new Error(NO_TOTAL_PAGES_ERROR);
}
const [cursor, setInternalCursor] = createSignal(props.initialCursor || 0);
const setCursor = (newCursor) => {
if (newCursor >= 0 && newCursor < props.totalPages) {
setInternalCursor(newCursor);
}
};
const goNext = () => {
const nextCursor = cursor() + 1;
setCursor(nextCursor);
};
const goPrev = () => {
const prevCursor = cursor() - 1;
setCursor(prevCursor);
};
createEffect(on(cursor, (value) => props.onChange?.(value), {defer: true}));
return {
cursor,
totalPages: props.totalPages,
goNext,
goPrev,
};
}
const Pagination = (props) => {
const {cursor, totalPages, goNext, goPrev} = paginationLogic(props);
const [bufferGap, setBufferGap] = createSignal(0);
const buffer = new Array(props.pagesBuffer).fill(0);
createEffect(() => {
debugComputation();
let newBufferGap = 0;
if (props.totalPages - cursor() < buffer.length) {
newBufferGap = props.totalPages - cursor() - buffer.length;
}
setBufferGap(newBufferGap);
});
return (
<div>
<button onClick={goPrev} disabled={cursor() === 0}>
PREV
</button>
<For each={buffer}>
{(item, index) => {
const pageCursor = () => cursor() + index() + bufferGap();
const className = () => (pageCursor() === cursor() ? 'selected' : '');
return pageCursor() >= 0 && pageCursor() < totalPages ? (
<span class={className()}>{` [${pageCursor()}] `}</span>
) : null;
}}
</For>
<button onClick={goNext} disabled={cursor() === totalPages - 1}>
NEXT
</button>
</div>
);
};
export default Pagination;
What I’d like to do is convert it to TypeScript.
Shouldn’t be hard, right? :)
The approach I’m gonna take here is not replacing the existing component, but rather creating a new one which can “live” alongside the old one. This means that our project should also support plain old javascript.
I start with cloning the component directory, and calling it PaginationTS
.
Next step will be changing the extension of the component file to tsx
- so now it will be Pagination.tsx
.
When I do these changes the Component’s code lights up with errors, but before attending to them, let’s put the new component onto the screen, shall we?
BTW, this project was initialized with the SolidJS scaffolding (see the “getting started” page, but I didn’t use the TS version of it. I change the main App to be App.tsx and add the component to the main App, alongside the old component, and run yarn start
...
import Pagination from './components/Pagination/Pagination';
import PaginationTs from './components/PaginationTS/Pagination';
import styles from './App.module.css';
import {attachDebugger} from '@solid-devtools/debugger';
function App() {
attachDebugger();
return (
<div class={styles.App}>
<Pagination
totalPages={10}
pagesBuffer={5}
onChange={(newCursor) => console.log('newCursor :>> ', newCursor)}
/>
<PaginationTs
totalPages={10}
pagesBuffer={5}
onChange={(newCursor) => console.log('newCursor :>> ', newCursor)}
/>
</div>
);
}
export default App;
Yes, both components are active on the same page:
Take Up Thy Stethoscope And Walk
It’s time to put some TS magic in. The first thing I do is to add a tsconfig.json file with the following:
{
"compilerOptions": {
"jsx": "preserve",
"jsxImportSource": "solid-js",
"noImplicitAny": true,
"target": "ES6",
"moduleResolution": "node",
"allowJs": true,
"outDir": "build"
}
}
Here is what’s going on here real quick -
I instruct the TSC not to mess with JSX and import the types for JSX from solid-js. That, my friends, quiets down all the initial “noise” we have from the TSX component. In addition to that, I’m disallowing implicit “any”s and setting the target compilation to ES6 to be able to use those shiny APIs.
The last part is setting the module resolution to node, so that my modules could look for their dependencies in node_modules (read more about it here).
The “allowJS” allows us to, well, use plain old JS alongside TS. This means our project is in hybrid mode.
Saving this configuration now lights up my SolidJS component with different errors - real TypeScript problems. We can now attend to them starting with this one -
Component props type
Our first error is the implicit “any” type on the component’s props:
We need to define what type of props the component expects, and this will help us get autocomplete and a sort of validation for the props.
To help us with that we have the “Component” SolidJS type, which is basically a generic type for functional components.
I first define the PaginationProps type:
type PaginationProps = {
cursor: number;
initialCursor: number;
totalPages: number;
pagesBuffer: number;
onChange: (value: number) => void;
};
Notice that currently all the properties are required (none is optional) and that the onChange expects to have a value of type number as an argument.
I change my component declaration to use it:
const Pagination: Component<PaginationProps> = (props) => {
And that eliminates the type error I had on the component props, but…
Moving back to the App.tsx
and I get new errors, can you guess them?
Yes, since my component now requires all the props to be available, when I’m not supplying all of them, as you can see in the image above, I get this wonderful error message.
I don’t want to add additional properties when not needed, so I’m going to change the type in order to make them optional, using the “?”:
type PaginationProps = {
cursor?: number;
initialCursor?: number;
totalPages: number;
pagesBuffer: number;
onChange?: (value: number) => void;
};
All I’m saying here, that when using this component, it has to have totalPages
and pageBuffer
. The rest of it? Up to whomever uses it.
Cool thing about it so far is that now I have autocomplete over the Pagination props:
Moving forward -
Signals
I have a function within my Pagination component, which is responsible for supplying the logic behind the component. You can regard it as a “hook” if you’re coming from the React realms. This function gets some props and returns computed values and methods which then are used for the component’s interaction handling.
Same as before, this function argument has an implicit “any” type to it:
So fixing it is pretty simple, since the type should be the PaginationProps:
function paginationLogic(props: PaginationProps) {
Let’s dive into the function’s body.
We’re using Solid’s Signals here to make the cursor reactive. Let’s TypeScript that. This is the original line:
const [cursor, setInternalCursor] = createSignal(props.initialCursor || 0);
The value we’re going to store in the signal is of number type, so let’s declare that:
const [cursor, setInternalCursor] = createSignal<number>(props.initialCursor || 0);
Now we can create the PaginationLogicType
the function paginationLogic
function returns:
type PaginationLogicType = {
cursor: Accessor<number>;
totalPages: number;
goNext: () => void;
goPrev: () => void;
};
The type we give the cursor
property is the Accessor generic with a number type, which basically stands for a function which returns the generic type assigned.
You can read more about signal types here.
Some minor typings
The implicit “any” type points my attention to another method which has an argument with no type - the setCursor
method:
I know that the “newCursor” arg should be a number so let’s type it:
const setCursor = (newCursor: number) => {
if (newCursor >= 0 && newCursor < props.totalPages) {
setInternalCursor(newCursor);
}
};
Style modules
This one is not directly related to SolidJS, but is still annoying so let’s solve that as well.
In this component I’m using CSS modules, like so:
import styles from './Pagination.module.css';
And TypeScript, well, it does not know how to treat these file extensions. It will simply spit this error message:
"Cannot find module './Pagination.module.css' or its corresponding type declarations.",
The way to solve that is to introduce our own custom types declaration which declares a module which applies to all “*.module.css” files. For that I created a file called “typing.d.ts” on the project’s root, and added this content to it:
declare module '*.module.css' {
const classes: {[key: string]: string};
export default classes;
}
That solves this issue and I think that at this point I can safely say that the Pagination component was successfully converted to TypeScript :)
Wrapping up
The Pagination component now supports typings. We went over typing the component function, the signals inside of it, some minor implicit “any” issues and also the annoying style modules type. Here is the final component code, which you can also find on my SolidJs-Lab at Github:
import {Accessor, Component, createEffect, createSignal, For, on} from 'solid-js';
import styles from './Pagination.module.css';
import {debugComputation} from '@solid-devtools/logger';
type PaginationProps = {
cursor?: number;
initialCursor?: number;
totalPages: number;
pagesBuffer: number;
onChange?: (value: number) => void;
};
type PaginationLogicType = {
cursor: Accessor<number>;
totalPages: number;
goNext: () => void;
goPrev: () => void;
};
const NO_TOTAL_PAGES_ERROR = 'No total pages were given';
function paginationLogic(props: PaginationProps): PaginationLogicType {
if (!props.totalPages) {
throw new Error(NO_TOTAL_PAGES_ERROR);
}
const [cursor, setInternalCursor] = createSignal<number>(props.initialCursor || 0);
const setCursor = (newCursor: number) => {
if (newCursor >= 0 && newCursor < props.totalPages) {
setInternalCursor(newCursor);
}
};
const goNext = () => {
const nextCursor = cursor() + 1;
setCursor(nextCursor);
};
const goPrev = () => {
const prevCursor = cursor() - 1;
setCursor(prevCursor);
};
createEffect(on(cursor, (value) => props.onChange?.(value), {defer: true}));
return {
cursor,
totalPages: props.totalPages,
goNext,
goPrev,
};
}
const Pagination: Component<PaginationProps> = (props) => {
const {cursor, totalPages, goNext, goPrev} = paginationLogic(props);
const [bufferGap, setBufferGap] = createSignal(0);
const buffer: number[] = new Array(props.pagesBuffer).fill(0);
createEffect(() => {
debugComputation();
let newBufferGap = 0;
if (props.totalPages - cursor() < buffer.length) {
newBufferGap = props.totalPages - cursor() - buffer.length;
}
setBufferGap(newBufferGap);
});
return (
<div class={styles.pagination}>
<button onClick={goPrev} disabled={cursor() === 0}>
PREV
</button>
<For each={buffer}>
{(item, index) => {
const pageCursor = () => cursor() + index() + bufferGap();
const className = () => (pageCursor() === cursor() ? styles.selected : '');
return pageCursor() >= 0 && pageCursor() < totalPages ? (
<span class={className()}>{` [${pageCursor()}] `}</span>
) : null;
}}
</For>
<button onClick={goNext} disabled={cursor() === totalPages - 1}>
NEXT
</button>
</div>
);
};
export default Pagination;
As always, if you think I missed anything, have questions or comments, be sure to share them in the comments section below :)
Hey! If you liked what you've just read check out @mattibarzeev on Twitter 🍻
Photo by vackground.com on Unsplash
Top comments (3)
Hi. You might want to take a look at our new pagination primitive: github.com/solidjs-community/solid...
I wrote it after seeing this article and thinking about how to solve pagination in an idiomatic, yet unopiniated way.
That's cool mate! Basically extracting the pagination logic, much like a "hook" in React. Nice :)
I think that SolidJs shines in that it is very intuitive to extract parts of a component logic this way
Solid comes with a flexibility that will easily seep into your own libraries and primitives if you start to understand the problem you want to solve in the context of Solid and its reactive system.
Pagination is a very nice example, because it consists of only 3 reactive paradigms and not much more:
You only want solid's reactivity trigger if at least one of those change, so that's exactly what I do:
A nice touch (even though I say that myself) was the addition of "page" as a non-enumerable property of the props objects: this way we can use a props destructure into the JSX without it polluting the DOM. That's a pattern I will try use more often when working with externalized props.