Convert Your SolidJS Component To TypeScript
Tags: solidjs, typescript, tutorial, webdev
Published: 2022-12-02 10:26:12

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](https://dev.to/mbarzeev/converting-a-react-component-to-solidjs-5bgj), 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:

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/98va2vycaygq86w3hmm6.png)

It’s currently written in plain JS and looks like this:

```javascript
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](https://www.solidjs.com/guides/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`...

```javascript
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:

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/0xsz7mxkmrweicyqkmkn.png)

## 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:

```json
{
   "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](https://www.typescriptlang.org/docs/handbook/module-resolution.html#how-nodejs-resolves-modules)).
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:

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/p749woa4clyzac2vxrac.png)

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](https://www.solidjs.com/guides/typescript#component-types)” SolidJS type, which is basically a generic type for functional components.
I first define the PaginationProps type:

```javascript
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:

```javascript
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?

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/5ok5wbqmgfq3nje0yq9d.png)

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 “?”:

```javascript
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:

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/biphqr8mztsv3lt5zw2w.png)

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:

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/k73nwzq2sb9y6xkz7fwb.png)

So fixing it is pretty simple, since the type should be the PaginationProps:

```javascript
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:

```javascript
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:

```javascript
const [cursor, setInternalCursor] = createSignal<number>(props.initialCursor || 0);
```

Now we can create the `PaginationLogicType` the function `paginationLogic` function returns:

```javascript
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](https://www.solidjs.com/guides/typescript#signals).

## Some minor typings 
The implicit “any” type points my attention to another method which has an argument with no type - the `setCursor` method:

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/vu095bbigz5wpyvsmb80.png)

I know that the “newCursor” arg should be a number so let’s type it:

```javascript
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:

```json
"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:

```javascript
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](https://github.com/mbarzeev/solid-js-lab):

```javascript
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 <a href="https://twitter.com/mattibarzeev?ref_src=twsrc%5Etfw" class="twitter-follow-button" data-show-count="false">@mattibarzeev</a><script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script> on Twitter* :beers:

<small><small><small>Photo by <a href="https://unsplash.com/@vackground?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText">vackground.com</a> on <a href="https://unsplash.com/s/photos/different-types?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText">Unsplash</a></small></small></small>