loading...

How to use Typescript with Svelte

codechips profile image Ilia Mikhailov Originally published at codechips.me on ・8 min read

Not to fear, Typescript fans! Even though Svelte lacks first class TS support, turns out that you can actually use Typescript to some degree with Svelte even today. The only things required are proper tools and plugins. Read on to find out how.

Boilerplate

One of the show stoppers for people who want to start using Svelte is the lack of first class Typescript integration. And it's not so much about the type safety as about tooling. Great supporting tools for any framework are important for its future growth and popularity. Svelte is still a young framework, but without proper tooling ecosystem surrounding it, I am afraid it might die. It would be such shame.

I've done some experimenting with Rollup, Webpack and Parcel. While I achieved somewhat decent result with all of them, I will use Rollup here as it had a pretty straight forward setup and also the re-compilation step was the fastest of them all.

Let's start of with a standard Svelte setup and adjust from there. The best way to learn is by doing.

$ npx degit npx degit sveltejs/template svelte-and-typescript
$ cd svelte-and typescript && yarn && yarn upgrade --latest

We now have a simple Svelte app, with all the greatest and latest dependencies, that we can run with yarn dev.

Rollup refactoring

I prefer a slightly different Rollup configuration so we will adjust it a bit to my liking. It requires us to bring in a few new utilities first. We will start with them.

$ yarn add -D rollup-plugin-serve rollup-plugin-html2 del-cli

Time to refactor our rollup.config.js to something more readable.

import commonjs from '@rollup/plugin-commonjs';
import html from 'rollup-plugin-html2';
import livereload from 'rollup-plugin-livereload';
import resolve from '@rollup/plugin-node-resolve';
import serve from 'rollup-plugin-serve';
import svelte from 'rollup-plugin-svelte';
import { terser } from 'rollup-plugin-terser';

const isDev = process.env.NODE_ENV === 'development';
const port = 3000;

// define all our plugins
const plugins = [
  svelte({
    dev: isDev,
    extensions: ['.svelte'],
  }),
  resolve({
    browser: true,
    dedupe: ['svelte'],
  }),
  commonjs(),
  // injects your bundles into index page
  html({
    template: 'src/index.html',
    fileName: 'index.html',
  }),
];

if (isDev) {
  plugins.push(
    // like a webpack-dev-server
    serve({
      contentBase: './dist',
      historyApiFallback: true, // for SPAs
      port,
    }),
    livereload({watch: './dist'})
  );
} else {
  plugins.push(terser({ sourcemap: isDev }));
}

module.exports = {
  input: 'src/main.js',
  output: {
    name: 'bundle',
    file: 'dist/bundle.js',
    sourcemap: isDev,
    format: 'iife',
  },
  plugins,
};

Alright, Rollup config done. Now we need to fix our package.json too. Replace your "scripts" property with the following content.

{ 
  "start": "del-cli dist && NODE_ENV=development rollup --config --watch", 
  "build": "del-cli dist && NODE_ENV=production rollup --config"
}

Also put a minimal index.html file in the src directory with the following content.

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset='utf-8'>
    <meta name='viewport' content='width=device-width,initial-scale=1'>
    <title>svelte app</title>
  </head>
  <body></body>
</html>

We, can now run and build our app with yarn start and yarn build respectively. Give it a try!

From JS to TS

It's time to bring out the big guns - Typescript. For this to work we need to add some more modules - rollup/plugin-typescript, typescript and tslib , which is a dependency for Rollup's typescript plugin.

$ yarn add -D typescript tslib @rollup/plugin-typescript

Done? Good! Now we have to create a minimal tsconfig.json

{
  "include": ["src/**/*"],
  "exclude": ["node_modules/*"],
  "compilerOptions": {
    "target": "es2017",
    "types": ["svelte"],
    "moduleResolution": "node"
  }
}

We also need to add Typescript support to our Rollup config.

// add typescript plugin to imports
import typescript from '@rollup/plugin-typescript';

// and replace plugins section with this
const plugins = [
  svelte({
    dev: isDev,
    extensions: ['.svelte']
  }),
  typescript(),
  resolve({
    browser: true,
    dedupe: ['svelte'],
  }),
  commonjs(),
  html({
    template: 'src/index.html',
    fileName: 'index.html',
  }),
];

We will now test-drive this with an actual Typescript file. Create a timer.ts file in your src folder.

import { readable } from 'svelte/store';

const timer = readable(0, (set) => {
  let current: number = 0;

  const id = setInterval(() => {
    current += 1;
    set(current);
  }, 1000);

  return () => clearInterval(id);
});

export { timer };

Let's require it in our App.svelte and see if it works.

<!-- App.svelte -->

<script>
  import { timer } from './timer';
</script>

<main>
  <h2>Count is {$timer}</h2>
</main>

Fire up the app and see if it works. Hint: it should.

Svelte files in Typescript files

Try renaming your main.js to main.ts Don't forget to change the entry prop in Rollup config. When we start the app it should also work as expected. Achievement unlocked. We now can work with Typescript files in our Svelte projects!

What about Typescript in Svelte files?

Ah! Of course! Glad you asked. It's possible too. For that you have to use the awesome svelte-preprocess plugin. It's a plugin that can help you pre-process many different kinds of languages in Svelte files such as SASS, Less, Pug and Typescript.

$ yarn add -D svelte-preprocess

Tell Svelte plugin to use preprocess in our rollup.config.js

// import preprocess
import preprocess from `svelte-preprocess`;

// add preprocess to Svelte config
  svelte({
    dev: isDev,
    extensions: [".svelte"],
    preprocess: preprocess()
  })

Now, in our App.svelte we can change the script tag from default js to Typescript. To test that it actually works we will add a variable with a type.

<script lang="typescript">
  import { timer } from './timer';

  let a: number = 42;
</script>

<main>
  <h2>Count is {$timer}</h2>
  <p>What's the meaning of life? {a}</p>
</main>

Alright, by using Typescript we gained some, but we also lost some. What did we lose? Svelte's auto-subscriptions. You cannot do this for example. Typescript will not understand.

<script lang="typescript">
  import { timer } from './timer';

  let a: number = 42;
  $: sum = a + $timer;
</script>

We can however round the problem by managing subscriptions manually. Like this.

<script lang="typescript">
  import { onMount } from 'svelte';
  import { timer } from './timer';

  let a: number = 42;
  let current: number = 0;
  let sum: number = 0;

  // subscribe to store manually in onMount lifecycle hook
  // timer.subscribe returns an unsubscribe function that will
  // automatically be called by Svelte in the onDestroy hook
  onMount(() => timer.subscribe(val => (current = val)));
  R(timer, val => (current = val));

  // we have to define our sum var explicitly
  $: sum = a + current;
</script>

<main>
  <h2>Count is {$timer}</h2>
  <p>What's the meaning of life? {a}</p>
  <p>The sum is {sum}</p>
</main>

So this works pretty nicely and as we saw we gained some and we lost some. We gained type safety, but now our code need to be more explicit. We can abstract that further by creating a small helper utility that will make our code a little more concise.

Create a utils.ts in your source directory and paste this code.

import { onMount } from 'svelte';
import { Readable } from 'svelte/store';

const R = <T>(store: Readable<T>, callback: (value: T) => void) => {
  onMount(() => {
    return store.subscribe(callback);
  });
};

export { R };

Now we are able to reduce or abstract the code of our readable store. We can also do similar for writable and derived stores too if we feel like it.

<script lang="typescript">
  import { onMount } from 'svelte';
  import { timer } from './timer';
  import { R } from './utils';

  let a: number = 42;
  let current: number = 0;
  let sum: number = 0;

  //onMount(() => timer.subscribe(val => (current = val)));
  R(timer, val => (current = val));

  $: sum = a + current;
</script>

<main>
  <h2>Count is {$timer}</h2>
  <p>What's the meaning of life? {a}</p>
  <p>The sum is {sum}</p>
</main>

Our goal is now complete. We can write parts of our codebase in Typescript and we can also use Typescript in our Svelte files. However, if you have been coding along by copying and pasting code you might have seen that you get syntax errors in your editor (most likely VSCode or Vim). I personally find that slightly annoying. False positives. Luckily, it can be fixed, which leads us to the next part of the article.

Editor integration

You are most likely using VScode or Vim when coding. There are extensions for both of them - Svelte extension for VSCode and coc-svelte for Vim. However, your editor will not be able to understand Typescript in Svelte files out of the box, because those extensions know anything about any Typescript. We need to tell it how to process it.

VScode Syntax Error

Editor configuration always feels like black magic to me, but the most common recommendation is to create a svelte.config.js with the following content.

const { preprocess } = require('svelte-preprocess');

module.exports = {
  preprocess: preprocess(),
};

But that didn't work for me. Instead, I had to install @pyoner/svelte-ts-preprocess lib and use that instead.

// install the lib first
// yarn add -D @pyoner/svelte-ts-preprocess
const { preprocess } = require('@pyoner/svelte-ts-preprocess');

module.exports = {
  preprocess: preprocess(),
};

Restart your editor and everything should work as expected. Syntax errors be gone! Code editor will still complain that we have unused variables, but I can live with that. We can turn it off if it gets too annoying.

Bonus material

We can also install the "love-it-or-hate-it" Prettier plugin to help us out with code formatting.

$ yarn add -D prettier prettier-plugin-svelte

Create a Prettier config file and adjust to your needs.

// .prettierrc.js

module.exports = {
  tabWidth: 2,
  semi: true,
  singleQuote: true,
  printWidth: 120,
  plugins: ['prettier-plugin-svelte'],
  svelteSortOrder: 'styles-scripts-markup',
  svelteStrictMode: false,
  svelteBracketNewLine: true,
};

You can probably add other useful code linting plugins, but that's out of the scope for the article, so I will stop here.

Conclusion

As you see, we can get Typescript integration with Svelte even today with the right tools and plugins. The biggest hurdle is that all the surrounding tools are pretty outdated, with tons of issues and PRs hanging. No wonder, people are doing this outside work.

But, being an optimist, we might have bright future ahead of us. I read that there is a lot of activity around improving tools for Svelte, to try and gather them under the official Svelte umbrella, like here. Also, bringing in first class Typescript to Svelte discussion has intensified as well.

I recommend to keep your Svelte files thin and write all the logic in separate files instead. By utilizing libraries such as Xstate, RxJS and Rambda you can write very concise and testable code too. The bar is high, but it's totally worth it! Give them a fair chance!

Plugins mentioned

Before you go

You can find the code here https://github.com/codechips/svelte-and-typescript

If you feel that you need to have Typescript support for your next Svelte project, I already did the hard work for you.

$ npx degit codechips/svelte-starter-template#with-typescript my-app

Hope you learned something new with me today and if something is not right or can be improved, please ping me on Twitter or leave a comment.

Originally published on https://codechips.me/how-to-use-typescript-with-svelte/


Follow me on Twitter for new posts, useful links and byte-sized wisdom.

Discussion

pic
Editor guide