DEV Community

Cover image for Trying Out Type Safe Navigation in SvelteKit
qaynam
qaynam

Posted on • Edited on

Trying Out Type Safe Navigation in SvelteKit

Recently Switched from Next.js to SvelteKit and Loving It

Motivation

I wanted to implement tansack-router typing in Svelte like thisπŸ‘‡

nextjs-typesafe-route

So I built my own and wanted to document it here.

Implementation

SvelteKit puts a lot of emphasis on type safety between server and client. When you start the dev server, it generates various types for data returned by load functions in .sveltekit/types.

You can import and use them from the $types alias like this πŸ‘‡

load-type

https://kit.svelte.dev/docs/load#page-data

I thought I could do something similar with code generation. Looking through the SvelteKit source code, I found the create_manifest_data function




export default function create_manifest_data({
    config,
    fallback = `${runtime_directory}/components`,
    cwd = process.cwd()
}) {
    const assets = create_assets(config);
    const matchers = create_matchers(config, cwd);
    const { nodes, routes } = create_routes_and_nodes(cwd, config, fallback);

    for (const route of routes) {
        for (const param of route.params) {
            if (param.matcher && !matchers[param.matcher]) {
                throw new Error(`No matcher found for parameter '${param.matcher}' in route ${route.id}`);
            }
        }
    }

    return {
        assets,
        matchers,
        nodes,
        routes
    };
}


Enter fullscreen mode Exit fullscreen mode

github link

I created a codegen.js file in the project root. It runs node codegen.js when starting dev server to continually watch for file changes.




import create_manifest_data from './node_modules/@sveltejs/kit/src/core/sync/create_manifest_data/index.js';
import { load_config } from './node_modules/@sveltejs/kit/src/core/config/index.js';
import fs from 'node:fs';
import { format } from 'prettier';

const specialRouteIdPattern = /(\/(\(|\[).*(\]|\)))?/g;

async function getRoutes() {
    /** @type import('@sveltejs/kit').ValidatedConfig */
    const config = await load_config();

    /** @type import("@sveltejs/kit").ManifestData */
    const manifest = await create_manifest_data({ config });
    return manifest.routes;
}

async function generateRouteTypeFile(
    /** @type import("@sveltejs/kit").RouteData[] */
    routes
) {
    const ids = routes
        .filter(
            (route) => Boolean(route.page))
        .map((route) => route.id.split('').join('').replace(specialRouteIdPattern, '')).filter(Boolean);
    const type = `export type RouteList = ${ids.map((id) => `"${id}"`).join(' | ')}`;

    fs.writeFileSync('./src/lib/type.d.ts', await format(type, { parser: 'typescript' }));

    return { routes: ids };
}

const { routes } = await generateRouteTypeFile(await getRoutes());
console.log('Routes:');
console.table(routes);
console.log('Generated route type file βœ…');

console.log('Watching for changes...');
fs.watch('./src/routes', { recursive: true }, async (event, filename) => {
    console.log(`Change detected in ${filename} `);
    if (event === 'rename') {
        const routes = await getRoutes();
        await generateRouteTypeFile(routes);
        console.log('Generated route type file βœ…');
    }
});




Enter fullscreen mode Exit fullscreen mode

To briefly explain the code, we use a module that is not publicly available, get a route, exclude specific routes such as [.... .path] (gourp), generate the ts code, write it to the file, and keep waiting for the file to change.

When dev server starts, it generates src/lib/type.d.ts with route ids like:




export type RouteList = "/" | "/auth" | "/auth/forgot-password" | "/auth/reset-password" | "/auth/sign-in" | "/auth/sign-up"



Enter fullscreen mode Exit fullscreen mode

Since SvelteKit has $app/navigation instead of react-router, I wrapped it in lib/navigation.ts file like thisπŸ‘‡



import { goto } from '$app/navigation';

import type { RouteList } from '../routes/type';

export const navigate = (url: RouteList | URL, opt?: GotoParams[1]) => goto(url, opt);



Enter fullscreen mode Exit fullscreen mode

Now I get type hints like:

sample

Warning

This module is not exported from @sveltejs/kit, so it may stop working if destructive changes are made.

Top comments (3)

Collapse
 
evertt profile image
Evert van Brussel

Did / will / would you ever turn this into an npm package?

Collapse
 
qaynam profile image
qaynam

I posted on svelte's discord server and got a reply that there is already a package called vite-plugin-kit-routes.
I haven't tried it myself yet, but it looks pretty good.

Collapse
 
evertt profile image
Evert van Brussel

You have got to be kidding me. I spent so many hours yesterday basically trying to write the exact same plugin. Thanks for sharing man. I love having more type-safety and auto-completion everywhere.