Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Generated types for pages/endpoints #647

Open
Tracked by #11108
Rich-Harris opened this issue Mar 24, 2021 · 33 comments
Open
Tracked by #11108

Generated types for pages/endpoints #647

Rich-Harris opened this issue Mar 24, 2021 · 33 comments
Labels
Milestone

Comments

@Rich-Harris
Copy link
Member

Rich-Harris commented Mar 24, 2021

Ways that types in SvelteKit apps could be improved:

Implicit params and props for load functions (update: done)

<script context="module">
  /** @type {import('@sveltejs/kit').Load */
  export async function load({ params, fetch }) {
    // `params` automatically typed from filename (e.g. `src/routes/blog/[slug]`)
    const res = await fetch(`/blog/${params.slug}.json`);
    const { post } = await res.json();

    return {
      props: {
        // `post` type automatically inferred from component props
        post
      }
    };
  }
</script>

<script>
  /** @type {BlogPost} */
  export let post;
</script>

Similarly, with shadow endpoints, it would be good to type body based on component props (though this could be tricky since component props combine e.g. post and get bodies), and also type the props input to load in cases where it's used.

It might be possible to do something clever with rootDirs, or with preprocessors?

Typed goto and fetch

As mentioned below, it might be possible to type functions like goto based on the available routes. It probably gets tricky with relative routes, but that could be a bailout.

Typed links

This is a little tricky for us, since we use <a> instead of <Link>, but it would be neat if it was possible to typecheck links somehow.

@Rich-Harris

This comment has been minimized.

@Conduitry

This comment has been minimized.

@Rich-Harris

This comment has been minimized.

@Rich-Harris

This comment has been minimized.

@Conduitry

This comment has been minimized.

@Rich-Harris

This comment has been minimized.

@Rich-Harris

This comment has been minimized.

@PatrickG

This comment was marked as off-topic.

@ivanhofer
Copy link
Contributor

Originally posted in #3090, here I describe how goto, fetch (and like @ebeloded mentioned invalidate, prefetch, prefetchRoutes) could be improved with some type-information:

Describe the problem

In larger projects (also in smaller projects) it would be great if the goto and the fetch functions could offer more typesafety. Problems with missing typesafety are:

  • typos when you link to a route that does't exist e.g. /costumer instead of /customer
  • help for refactorings e.g. when renaming larger chunks of files inside the routes folder, all links need to be updated too
  • trying to call fetch with the wrong method e.g. using PUT instead of PATCH

It would be great if the goto and the fetch functions could output an error when you pass in a invalid relative slug.

Describe the proposed solution

The problem could be solved by providing advanced TypeScript types for the goto and the fetch function.
Similar tho the already generated .svelte-kit/dev/generated/manifest.js file, SvelteKit could generate a d.ts with types depending on the .svelte files inside the routes folder and depending on the function inside a .js and .ts Endpoints file.

These types then could be used to enhance the goto and fetch functions.
The typesafe functions could replace the existing import from app/navigation. I'm not sure how this could work for the fetch function since you don't really import it from anywhere.
Or this could be an additional function you need to import from app/typesafe or something similar.

Here is a working example how I think this could look like:

  • helper-types: for cheching if a slug is valid
type SplitPath<S extends string> =
	S extends `/${infer Part}/${infer Rest}`
		? ['/', Part, '/', ...SplitPath<Rest>]
		: S extends `${infer Part}/${infer Rest}`
			? [Part, '/', ...SplitPath<Rest>]
			: S extends `/${infer Part}`
				? ['/', Part]
				: S extends ''
					? []
					: S extends `${infer Part}`
						? [Part]
						: []

type RemoveEmptyEntries<A extends Array<unknown>> =
	A extends []
		? []
		: A extends [infer Item, ...infer Rest]
			? Item extends ''
				? RemoveEmptyEntries<Rest>
				: [Item, ...RemoveEmptyEntries<Rest>]
			: []
  • routes: for the goto function
// alias type to get better TypeScript hints inside tooltips
type id = string

// this type is dynamic and get's generated
type Routes =
	| ['/'] // index.svelte
	| ['/', 'about'] // about/index.svelte
	| ['/', 'products'] // products/index.svelte
	| ['/', 'products', '/', 'create'] // products/index.svelte
	| ['/', 'products', '/', id] // products/[id]/index.svelte
	| ['/', 'products', '/', id, '/', 'edit'] // products/[id]/edit.svelte

export type IsValidRoute<R extends string> =
	R extends `http${string}`
		? R
		: RemoveEmptyEntries<SplitPath<R>> extends Routes
			? R
			: 'No such Route'

const goto = <Route extends string>(href: IsValidRoute<Route>): void => {
	// TODO: goto href
}

// @ts-expect-error
goto('')
goto('/')
goto('/about')
// @ts-expect-error
goto('/invalid')
// @ts-expect-error
goto('/product')
goto('/products')
// @ts-expect-error
goto('/products/')
// @ts-expect-error
goto('/products/create/')
goto('/products/123456')
// @ts-expect-error
goto('/products/123456/')
// @ts-expect-error
goto('/products/123456/add')
goto('/products/123456/edit')
// @ts-expect-error
goto('/products/123456/5678/edit')

goto('https://kit.svelte.dev')
  • endpoints: for the fetch function
type Methods =
	| 'GET'
	| 'POST'
	| 'PUT'
	| 'PATCH'
	| 'DELETE'

// this type is dynamic and get's generated
type Endpoints = {
	GET:
	| ['/', 'products']
	| ['/', 'products', '/', id]
	POST:
	| ['/', 'products']
	PATCH:
	| ['/', 'products', '/', id]
}

export type IsValidEndpoint<M extends Methods, R extends string> =
	R extends `http${string}`
		? R
		: M extends keyof Endpoints
			? RemoveEmptyEntries<SplitPath<R>> extends Endpoints[M]
				? R
				: 'No such Endpoint'
			: 'No such Method'

const fetch = <Endpoint extends string, Method extends Methods = 'GET'>(endpoint: IsValidEndpoint<Method, Endpoint>, options?: { method?: Method, [key: string]: any }): void => {
	// TODO: call fetch
}

fetch('/products')
// @ts-expect-error
fetch('products')
fetch('/products/12345')
fetch('/products', { method: 'POST' })
// @ts-expect-error
fetch('/products', { method: 'PATCH' })
// @ts-expect-error
fetch('/products/12345', { method: 'POST' })
fetch('/products/12345', { method: 'PATCH' })

fetch('http://example.com/articles')

You can copy these examples to a .ts file and try passing some valid/invalid strings to the goto and fetch functions.
Lines annotated with // @ts-expect-error are invalid and will throw a TypeScript Error.

@ivanhofer
Copy link
Contributor

Today I thought about how this could be implemented. I took a look how the current types are referenced and how the topics discussed above could fit in.

I'm assuming that the "sveltekit dev process" processes the source files and then generates some .d.ts files somewhere inside the .svelte-kit directory

routing

here I'm always using goto but the same approach could be applied for all other functions

When looking at the goto function, the types are coming from @sveltejs/kit package referenced by the src/global.d.ts file.
With this approach we can't override types defined inside @sveltejs/kit with an "enhanced" version of goto.
A solution to this problem could be to reference a generated file inside global.d.ts`:

/// <reference types="../.svelte-kit/types" />

This file then imports some general SvelteKit type information and extends them with the generated types:
I'm currently not aware of a way to override a type defined inside @sveltejs/kit so in order to make this work the declaration of the goto function inside the $app/navigation module has to be removed from the @sveltejs/kit types.
The module then get's declared with the .svelte-kit/types file/directory.

/// <reference types="@sveltejs/kit" />

type IsValidRoute = /* ... */

declare module '$app/navigation' {
   export function goto<Route extends string>(href: IsValidRoute<Route>, opts): Promise<any>
   // ...
}

This would be a breaking change since all existing SvelteKit projects would need to update the global.d.ts file. The dev process could parse the content of that file and output an error mentioning that the reference needs to be changed.

When starting with a fresh project, the .svelte-kit folder is not present until the dev process get's started. So maybe the template needs to contain a simple .svelte-kit/types.d.ts file out-of-the-box.

load

When looking at the types for the ´load´ function, it would be great if the types could get injected automatically into the svelte routes.

enhance function

I think this would be possible via preprocessing the files to add the type. So you could write:

export async function load({ params }) {
  // params has type Record<string, string>
}

and the preprocessor would turn this into:

/** @type {import('./[slug]').Load} */
export async function load({ params }) {
  // params has type { slug: string }
}

inject type

It's probably a bad idea to look for an export named load, so maybe a better idea would be to automatically inject just the Load type. So you would write:

/** @type {Load} */
export async function load({ params }) { }

and the preprocessor would turn it into:

/** @typedef { import('./[slug]').Load} Load`,
/** @type {Load} */
export async function load({ params }) { }

So you won't actually import the Load type directly. It will be just there for you to use.

virtual module

Or maybe import it from a "virtual" package like sveltekit/types

/** @type {import('@sveltekit/types').Load} */
export async function load({ params }) { }

and the preprocessor turns it into:

/** @type {import('./[slug]').Load} */
export async function load({ params }) { }

conclusion

These are my thoughts. When thinking about the rootDirs solution from the first comment, the automatic injection of the Load type in some form could improve the DX a lot. As I'm heavily using the automatic import function that VS Code provides, I can immagine that having multiple Load types generated for each route would probably never import the type from the correct path.

What do you think?

@thenbe
Copy link
Contributor

thenbe commented Feb 17, 2022

There's new community package that addresses typesafety between endpoints and routes in sveltekit. I felt like it could be relevant to this discussion or atleast the folks following this thread. Tbh, I don't know much about its internals, but it uses tRPC to achieve said typesafety.

https://github.com/icflorescu/trpc-sveltekit

@ivanhofer
Copy link
Contributor

Ah great that there exists a package for trpc now, I hacked it together manually in a recent project of mine.
Thanks for sharing the link @ambiguous48

@thenbe thenbe mentioned this issue Mar 4, 2022
5 tasks
@Rich-Harris Rich-Harris modified the milestones: 1.0, post-1.0 Mar 5, 2022
@ivanhofer
Copy link
Contributor

ivanhofer commented Dec 7, 2022

I made some thoughts about this topic again, because I had a bug in one of my SvelteKit applications.
The issue was that I have used $page.params.id in one of my +page.svelte files. Then I renamed the route from [id] to [productId] and guess what: I forgot to change the id to productId on the $page.params object.

This is not a problem inside load functions because they get typed by the generated ./$types.d.ts file.
But in regular .svelte files you need to use the generic $page store or manually wrap it like this:

import type { RouteParams } from './$types.js'

($page.params as RouteParams).productId

Also accessing form, data, route.id on the $page object results in having a genericly typed object.
For form and data this is not really an issue since those objects get passed as page props, so you can specify the generated type there.

Possible solutions:

  1. add params to the page props

    <script>
    	import type { PageData, RouteParams } from './$types.js'
    
    	export let params: RouteParams
    	export let data: PageData
    </script>

    This would make it consistent to the other props that already get passed. This would probably mean a tiny runtime overhead.

  2. generate a file that wraps the $page object with the generated files

    <script>
    -	import { page } from '$app/stores'
    +	import page from './$page'
    	
    	$page.params.projectId
    </script>

    This would be a solution almost entirely on the typesystem-level, with no runtime overhead (just a reference to the original $app/stores object). This would also open up the possibility to export a goto function with enhanced typing like described above in a similar way.

  3. doing more magic in the language tools

    Let users write the regular $page/stores import. The language tools then check if it gets imported in a +page.svelte file and then replaces the types on-the-fly. I have no experience with such things but I would assume it is possible to create a TypeScript plugin that is able to replace the type per file basis.

    Additional to the $app/stores import, It would be great to have such a solution also for everything inside ./$types. Every now and then I accidentially import the generic RequestHandler from @sveltejs/kit instead of the better typed version from the generated ./$types.d.ts file. Having just a single possible RequestHandler to import and then letting the language tools do their magic, could be an option. And also a solution to such a problem:
    image
    As soon as you start having a few routes the "Quick Fix..." dialog in VS Code shows an endless list of possible types to import. For some reason ../$types alwas shows up above the ./$types that you probably want to import.

    This image shows the better working variant of this panel. Sometimes another version shows up, where the list is randomly sorted and you have to hover over the name in order to see the path. It is almost impossible to import a type from the correct path. I don't know why and when this variant shows up, but I guess it is a bug. I can't reproduce right now, but it shows up every other day. I'll post it here once it shows up the next time.

This all is just complaining at a high level from my side. I'm really happy how well the TypeScript support has improved in the last months. The points above are just minor issues I encounter using SvelteKit day by day. I just want to share it here with you.

[update]:

here comes the ugly version of the "Quick Fix..." dialog mentioned above. This is more like a "code completion" dialog. I just found out that this gets triggered by pressing CTRL + SPACE and the real "Quick Fix..." dialog pops up by pressing CTRL + . (on Windows). Triggering the "code completion" dialog does not always work. But you should see that it is almost impossible to import the correct type from there. This list is not sorted (or at least it seems so) and you need to select it via keyboard to see the actual path. In my opinion this is completely useless.

It would be great if the language-tools could find a way to modify this list e.g. sorting entries, adding more information to it, just showing a single entry or removing all entries alltogether.

image

@benmccann benmccann added the feature request New feature or request label Jan 20, 2023
@kwangure
Copy link
Contributor

We could introduce typed helper functions for html elements & relative routes.

Braindump:

<script>
    import { static, route, endpoint } from './$routes.js'; // bikeshedable
</script>

<img src={static("/favicon.jpg")}/>
<a href={route("/about")}>About</a>
<form action={endpoint("/api/v1/rsvp")>
     <button name="rsvp" value="yes">RSVP</button>
</form>

In dev mode, static and friends would use the TypeScript patterns suggested above, and possibly do some validation TypeScript can't (e.g relative routes). In build mode, they'd resolve to typed identity functions. We could preprocess them away entirely, though there may be some caveats (e.g function complicatedFunction(a) { return route(a) } or poor performance).

On the untyped $page.data and params @ivanhofer mentions above, as well as goto we might follow a similar pattern. Generated files import from $app/** and enhance with types.

<!--  /product/[productId]/+page.svelte  -->
<script>
    import { page } from './$stores.js';
    import { goto } from './$navigation.js';

    $: ({ params } = page); // `params: { productId: string; }`
	goto('/prodoct/123') // error
</script>

I use ./$routes.js because it matches the ./$types.js but it might be considered a breaking change. I highly doubt people use ./$routes.js in their app though. Alternatively, we could use ./+routes.js since + files are reserved, but we've taught SvelteKit users that ./$ stuff is generated and ./+ is stuff you write. If we went this way, it may also be a good idea to reserve ./$.

@ivanhofer
Copy link
Contributor

@kwangure I really like the idea of those helper functions.

I was just about to write another comment on this thread ^^.
What I want to mention is: SvelteKit could generate a metadata object/array of all routes, asset-paths etc. somewhere in the .sveltekit folder. From there on anyone can build their own helper functions e.g. create a goto function that only accepts absolute paths like mentioned a few comments above.
This would also open the possibilities for other use cases not thought of yet.

This should be a JavaScript object (as const to be able to infer types from it via typeof) and not a type because then you can do some other runtime stuff e.g. check how many parent layout.server.ts do exist and have a load function defined. I currently would need this in an application and without that I need to manually count and define a number on each function. It would make my life easier if I could just iterate over a metadata object to get to that number.

I haven't fully thought about this yet, but maybe I can write a utility function that uses vite's import.meta.glob to calculate that number.

SvelteKit probably already has those information and it would just need to expose them for other developers to use and build stuff on top.

@iandoesallthethings
Copy link

+1 for this. Right now, we have a super messy workaround to share types between standalone api endpoints and fetch helpers, but even just being able to import the RequestHandler's return type and wrap up fetch would be a lifesaver.

@AlbertMarashi
Copy link

AlbertMarashi commented Nov 22, 2023

Posting here from my discussion (#11042) and issue (#11101) as I was not aware that this existed.

I've come up with an approach that should work with Svelte's new +server.ts and +page.ts system.

Essentially we want a way to call our API endpoints so that we have typing automatically generated.

I came to much the same conclusion as many other people in this thread and developed a working prototype for how we could get typed API routes with a fetch wrapper

// Typed response returned by `GET` `POST` or other methods
export interface TypedResponse<A> extends Response {
    json(): Promise<A>;
}
import { json } from "@sveltejs/kit";
// A typed json wrapper (this could probably just replace `json`
export function typed_json<A>(data: A): TypedResponse<A> {
    return json(data)
}

Autogenerated api_fetch function

// autogenerated imports for the +server.ts files
import type * as AnotherAPI from "./api/another_api/+server";
import type * as ApiPostsPostParam from "./api/[post]/+server";

// Define a utility type that extracts the type a Promise resolves to
// from a function that returns a Promise.
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export type APIReturnType<T> = T extends (...args: any[]) => Promise<TypedResponse<infer O>> ? Promise<O> : never;

type Methods = "GET" | "POST" | "PUT" | "DELETE";

// These function overloads would be programmatically generated based on the manifest from +server.ts files
export async function api_fetch(url: "/api/another_api", method: "GET"): APIReturnType<typeof AnotherAPI.GET>
export async function api_fetch(url: "/api/[post]", method: "GET", params: { post: string }): APIReturnType<typeof ApiPostsPostParam.GET>
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export async function api_fetch(url: string, method: Methods, params?: Record<string, string>): Promise<any> {
    // parse the url
    // inject the params into the [param] sections
    const response = await fetch(url, { method });
    return response.json();
}

/api/[post]/+server.ts

import { typed_json, type TypedResponse } from "the_utils";
export async function GET({ params }): Promise<TypedResponse<{ text: string }>> /* the return type here could be auto inserted by the language server */ {
    return typed_json({ text: `The post ID is ${params.post}` }) satisfies TypedResponse<{ text: string }>;
}

+page.ts

import { api_fetch } from "./api_stuff";

export async function load() {
    const data = await api_fetch("/api/[post]", "GET", { post: "123" });

    return {
        text: data.text
    }
}

The prototype works and the IDE is able to pick up on the correct types as seen here:
image

The api_fetch signature could be adjusted, and even made to handle App.Error as well as potential typing for POST requests that provide JSON input, so we could potentially get a typed request.json() in the same way we get response.json() typed in this method

Thoughts?

@Lenard-0
Copy link

Lenard-0 commented Nov 22, 2023

@AlbertMarashi
I love this idea.

@jhwz
Copy link

jhwz commented Nov 22, 2023

Just a quick thought, I think we should keep as much in the type system as possible. If something like:

export async function api_fetch(url: string, method: Methods, params?: Record<string, string>): Promise<any> {
    // parse the url
    // inject the params into the [param] sections
    const response = await fetch(url, { method });
    return response.json() as never;
}

is implemented then you're hiding the fetch API. If you need to add anything to the request, or do anything else with the response then this helper will make that more difficult.

Projects I've made in the past which have delved into this sort of thing create types like:

type TypedResponse<Status extends number = number, H extends Headers = Headers> = Omit<
	Response,
	'status' | 'clone' | 'text' | 'formData' | 'json' | 'blob' | 'arrayBuffer' | 'headers'
> & {
	status: Status;
	headers: H;
	clone: () => TypedResponse<Status, H>;
};

type JSONResponse<R extends TypedResponse, Body> = Omit<R, 'clone'> & {
	json: () => Promise<Body>;
	clone: () => JSONResponse<R, Body>;
};

where essentially we're just modifying the return response type to type the json() function to not return Promise<unknown> but Promise<T>.

As discussed above, the harder part is generating types in all cases which match exactly one endpoint. In the easy case where you have two endpoints:

  • GET /api/endpoint1
  • GET /api/endpoint2

you might generate a fetch type like:

type Paths = '/api/endpoint1' | '/api/endpoint2'

type api_endpoint1_ResBody = { ...whatever }
type api_endpoint2_ResBody = { ...whatever }

type Fetch<P extends Paths> = (path: P, init?: RequestInit) => P extends '/api/endpoint1' ? JSONResponse<TypedResponse<200>, api_endpoint1_ResBody> : P extends '/api/endpoint2' ? JSONResponse<TypedResponse<200>, api_endpoint1_ResBody>  : never;

which can be used like

const f: Fetch<Paths> = fetch;
const res1 = f("/api/endpoint1")
res1.json();

This can easily be extended for different methods obviously, you're just generating a big-ass type. Route params makes all this more challenging

@AlbertMarashi
Copy link

AlbertMarashi commented Nov 23, 2023

@jhwz the problem is when you introduce params the typing for paths becomes complex and typescript does not have regex-based strings. I think it makes more sense to pass in params: { ... } and then parse those from the url template which looks like /api/posts/[post] or etc

In terms of stuff like the status & headers I think that seems like a good idea/extension. The api_fetch signature as I mentioned was only to demonstrate an example, but I think it could definitely use a similar API/signature as the fetch function, with possibly an added property for the params

interface TypedResponse<O> extends Response {
    json(): Promise<O>
}

type ApiPostsPostParamParams = {
    post: string
}

type Methods = "GET" | "POST" | "UPDATE" | "DELETE"
type FetchOptions<Params = never, Method extends Methods = Methods> =
    Parameters<typeof fetch>[1] & {
        params: Params,
        method: Method
    }

export async function api_fetch(url: "/api/posts/[post]", options?: FetchOptions<ApiPostsPostParamParams, "GET">): Promise<TypedResponse<{ foo: string }>>;
export async function api_fetch(url: string, options?: Parameters<typeof fetch>[1] | undefined): Promise<any> {
    
}

api_fetch("/api/posts/[post]", {
    method: "GET",
    params: {
        post: "post-id"
    }
})

TS Playground

This has the benefit of using a very similar fetch syntax, and we could also extend this to type the body property in FetchOptions via some kind of generic on export async function POST<{ foo: string}>/** inputs **/>(event) { ... }

@jhwz
Copy link

jhwz commented Nov 23, 2023

I see what you're getting at, I agree that keeping the type as the route ID and passing params is simpler. Good point!

@david-plugge
Copy link
Contributor

I currently use something like this:

import { resolveRoute } from '$app/paths';
import type RouteMetadata from '../../.svelte-kit/types/route_meta_data.json';
type RouteMetadata = typeof RouteMetadata;

type Prettify<T> = { [K in keyof T]: T[K] } & {};
type ParseParam<T extends string> = T extends `...${infer Name}` ? Name : T;

type ParseParams<T extends string> = T extends `${infer A}[[${infer Param}]]${infer B}`
	? ParseParams<A> & { [K in ParseParam<Param>]?: string } & ParseParams<B>
	: T extends `${infer A}[${infer Param}]${infer B}`
		? ParseParams<A> & { [K in ParseParam<Param>]: string } & ParseParams<B>
		: {};

type RequiredKeys<T extends object> = keyof {
	// eslint-disable-next-line @typescript-eslint/ban-types
	[P in keyof T as {} extends Pick<T, P> ? never : P]: 1;
};

export type RouteId = keyof RouteMetadata;

export type Routes = {
	[K in RouteId]: Prettify<ParseParams<K>>;
};

export function route<T extends keyof Routes>(
	options: {
		routeId: T;
		query?: string | Record<string, string> | URLSearchParams | string[][];
		hash?: string;
	} & (RequiredKeys<Routes[T]> extends never ? { params?: Routes[T] } : { params: Routes[T] })
) {
	const path = resolveRoute(options.routeId, options.params ?? {});
	const search = options.query && new URLSearchParams(options.query).toString();
	return path + (search ? `?${search}` : '') + (options.hash ? `#${options.hash}` : '');
}

Usage:

<script>
  import { route } from '$lib/route'

  route({
  routeId: '/(app)/posts/[postId]/edit',
  params: {
    postId: 'whatever'
  }
})
</script>

<a
  href={route({
    routeId: '/(app)/posts/[postId]/edit',
    params: {
      postId: 'whatever'
    }
  })}
>
  Whatever
</a>

Works pretty well! Not sure how well it scales as its parsing the params using typescript magic, but so far i did not have any issues.
Previously i created a vite plugin to generate the types, but then i noticed route_meta_data.json in the generated folder.

@PatrickG
Copy link
Member

@david-plugge this might be relevant for #11406

@Lootwig
Copy link

Lootwig commented Jun 16, 2024

@david-plugge that's awesome, I didn't think to leverage sveltekits internal route knowledge!

technical question, have you considered allowing to omit (group) segments for convenience?

style question, any particular reason for making the mandatory routeId a named prop, rather than just a first positional argument of route() (and options actually optional, then)?

@jhwheeler
Copy link

jhwheeler commented Jun 17, 2024

@Lootwig Do you mean this usage for the style question?

route('/(app)/posts/[postId]/edit',
  {
    routeId: ,
    params: {
      postId: 'whatever'
    }
  }
)

@AlbertMarashi
Copy link

AlbertMarashi commented Jun 17, 2024

@Lootwig Do you mean this usage for the style question?

route('/(app)/posts/[postId]/edit',
  {
    routeId: ,
    params: {
      postId: 'whatever'
    }
  }
)

In that scenario, with my approach in #11108 it makes sense to remove the (group) since it's not technically a valid URL

@david-plugge
Copy link
Contributor

david-plugge commented Jun 20, 2024

@Lootwig

technical question, have you considered allowing to omit (group) segments for convenience?

Thats absolutely possible:

type RemoveGroups<T> = T extends `${infer A}/(${string})/${infer B}`
	? `${RemoveGroups<A>}/${RemoveGroups<B>}`
	: T;
	
export type RouteId = RemoveGroups<keyof RouteMetadata>;

style question, any particular reason for making the mandatory routeId a named prop, rather than just a first positional argument of route() (and options actually optional, then)?

That works too, the main reason i had was that its a bit more complicated. But as in most cases you dont pass in a parameter i like this better.

type OptionalOptions<T extends RouteId> = {
	query?: string | Record<string, string> | URLSearchParams | string[][];
	hash?: string;
	params?: Routes[T];
};
type RequiredOptions<T extends RouteId> = {
	query?: string | Record<string, string> | URLSearchParams | string[][];
	hash?: string;
	params: Routes[T];
};

type RouteArgs<T extends RouteId> =
	RequiredKeys<Routes[T]> extends never
		? [options?: OptionalOptions<T>]
		: [options: RequiredOptions<T>];

export function route<T extends RouteId>(routeId: T, ...[options]: RouteArgs<T>) {
	const path = resolveRoute(routeId, options?.params ?? {});
	const search = options?.query && new URLSearchParams(options.query).toString();
	return path + (search ? `?${search}` : '') + (options?.hash ? `#${options.hash}` : '');
}

@david-plugge
Copy link
Contributor

Complete file: Sveltekit typesafe routes

@FilipLjubic
Copy link

Would it make sense to have a rune $route for something like this?

@ajhaupt7
Copy link

ajhaupt7 commented Aug 9, 2024

This is great stuff -- one small issue I noticed is that if you're using Sveltekit param matchers (ie routes/fruits/[page=fruit]) the typing here would expect you to do

route('/fruits/[page=fruit]',
  {
    params: {
      'page=fruit': 'whatever'
    }
  }
)

Sveltekit's resolveRoute expects you to pass the param as page rather than page=fruit, so it won't generate the route param correctly. To fix this you can change line 9 in this file to be

type ParseParam<T extends string> = T extends `...${infer Name}=${string}`
	? `...${Name}`
	: T extends `...${infer Name}`
		? `...${Name}`
		: T extends `${infer Name}=${string}`
			? Name
			: T;

This was referenced Sep 2, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests