How to add JSDoc Typechecking to SvelteKit

swyx

3 reactions 2022-02-04

As I build out swyxkit, I am finding that I am no longer prototyping and that I need to be able to refactor with confidence.

This makes it the right time to introduce types!

I was tempted to use TypeScript since I am more familiar with that, but since SvelteKit is written with JSDoc types, I figured I’d finally give that a try as I had never tried it before.

image

Verdict

You can see my successful conversion attempt here: sw-yx/swyxkit#23/

image

I’m pleased to report that I think JSDoc Typechecking is a perfect match for light-logic projects like this blog starter - when you don’t want to slow your builds down to strip out types, and you don’t REALLY need to strictly typecheck every little thing. IMO you should basically aim to typecheck just the riskiest parts of your code, which are mostly function boundaries, API responses and non-string values like Date.

The whole process took me about 30 mins and I already found myself experiencing the benefits of typechecking as I tweaked the return signatures of some functions and changed the props of some components (amazing work by @dummdidumm who maintains the Svelte Language Server - it can even detect when required props are missing!)

What follows here is an intro to Just Enough JSDoc Typechecking so you can get going with this on your own project.

Step 1: Simple variable declaration typing

The most basic jsdoc typing you can do is typing variable declarations. Since JS does implicit string conversion, I think you get the most mileage out of typing any component prop that isn’t a string.

<script>
	// FeatureCard.svelte
	/** @type {Date} */
	export let date;
</script>

This just helps prevent ugly unintended stringification.

The SvelteKit docs also strongly encourage you to annotate the SvelteKit APIs in pages:

<script context="module">
	// src/routes/__layout.svelte
	/** @type {import('@sveltejs/kit').Load} */
	export async function load({ url }) {
		return {
			props: {
				origin: url.origin
			}
		};
	}
</script>

and API routes:

// src/routes/api/blog/[slug].json.js

/**
 * @type {import('@sveltejs/kit').RequestHandler}
 */
export async function get() {
	return {
		body: { /* etc */},
		headers: {
			'Cache-Control': `max-age=0, s-max-age=${60}` // 1 minute.. for now
		}
	};
}

Of course, most of your data is gonna come with more complex types, which you will need to reuse…

Step 2: Make a .d.ts file

You’re going to pull your types, particularly reusable types, from this file. Rich says you should put it in /src/lib/types.d.ts so you can take advantage of SvelteKit’s aliasing. Stub out your types and build them up as you convert the rest of the files…

// /src/lib/types.d.ts
export type GHComment = {
	// etc
}

export type GHMetadata = {
	// etc
}

Step 3: Imported typing for variable declaration

Now you can annotate more complex component props:

<script>
	// Comment.svelte
	/** @type {import('$lib/types').GHComment} */
	export let comment;
</script>

that wasn’t too bad! Go through all your Svelte files and annotate; you’ll probably find that you will need to flesh out your .d.ts file types as you go. However, you’ll still need to annotate API responses for full safety…

Step 4: Typing Node.js functions

Node.js code probably has the strongest reasons to properly annotate functions since this has the most data risk - you wanna be warned any time the type signature of inputs or outputs change. Here’s how to do it:

/**
 * @param {import('$lib/types').GithubIssue} issue
 * @returns {import('$lib/types').ContentItem} 
 */
function parseIssue(issue) {
   // ...
}

If you are parsing data and just need to annotate inline functions, that’s possible too:

// before
issues.forEach((issue) => {/* etc */})

// after
issues.forEach(
	  /** @param {import('$lib/types').GithubIssue} issue */
	  (issue) => {
	  }
)

Date sorting needs a bit of special love and care:

// normal
_allBlogposts.sort((a, b) => b.date - a.date); // Error: The left-hand side of an arithmetic operation must be of type 'any', 'number' or an enum type

// ts
_allBlogposts.sort((a, b) => b.date.valueOf() - a.date.valueOf()); // use valueOf to make TS happy https://stackoverflow.com/a/60688789/1106414

Step 5: Run your code!

You can do steps 1-4 entirely in the editor, relying on the language server, but the real test of whether or not you broke anything is just running your code. I caught two big mistakes in my refactor, mostly to do with not properly typing my API responses.

Further reading

Get updates on new posts and projects

3000+ subscribers including my Mom – see past issues

Reactions: 👍 3
Webmentions
Loading...