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

Infer types when calling endpoints through fetch wrapper inside load functions #9732

Closed
Tracked by #11108
boyeln opened this issue Apr 20, 2023 · 4 comments
Closed
Tracked by #11108

Comments

@boyeln
Copy link
Contributor

boyeln commented Apr 20, 2023

Describe the problem

It would increase ergonomics if the return type of fetch calls to custom endpoints (+server.js) was inferred inside load functions when using the provided fetch function.

I'm creating an application that needs to have a available APIs for integration purposes. I also have a database that can only be queried server side. Currently I can use my endpoints (+server.js) in my load functions using the provided fetch wrapper, and SvelteKit will automatically figure out if it should use HTTP to talk to the API, or just call the +server.js directly (which is awesome!):

// src/routes/api/books/+server.js

import { json } from "@sveltejs/kit";

export async function GET() {
  /** @type {{ title: string; author: string; }[]} */
  const books = await db.query(/* query */);
  return json(books);
}

// src/routes/books/+page.js

import { error } from "@sveltejs/kit";

export async function load({ fetch }) {
  /** @type {{ title: string; author: string; }[]} */
  const books = fetch("/api/books")
    .then(res => res.json())
    .catch(() => {
      throw error(500, "Failed to fetch books");
    });

  return { books };
}

However, as you can see, I have to type out the result of the fetch to my endpoint (+server.js). This is obviously not a huge deal, but it can get quite tedious and error prone when the application size grows.

Describe the proposed solution

Ideally I would like the provided fetch wrapper inside load function (in +page[.server].js) to infer the type when querying internal endpoints.

If I have this +server.js file inside src/routes/api/books/:

import { json } from "@sveltejs/kit";

export async function GET() {
  /** @type {{ title: string; author: string; }[]} */
  const books = await db.query(/* query */);
  return json(books);
}

Then I would have these results in a +page[.server].js file:

export async function load({ fetch }) {
  const books = await fetch("/api/books").then(res => res.json())
  //      ^ { title: string; author: string; }[]
}

This raises some questions however. What will happen if you do fetch("/api/books").then(res => res.formData())? Will it error? And what if you want to do more fancy things inside your endpoint. For example provide JSON response only if some specific headers are set?

Alternatives considered

It is possible to turn all +page.js files that uses the API into +page.server.js files, and query the database directly. However, the downside of that is that it's quite easy for your APIs and your internal usage of the database to shift. By dogfooding the API, it's easier to maintain it over time.

Another approach (which is how we are doing it currently) is to use tRPC as an abstraction layer:

// src/routes/api/books/+server.js

import { json } from "@sveltejs/kit";
import { trpc } from "$lib/trpc";

export async function GET({ fetch }) {
  const books = await trpc(fetch).books.all.query();
  return json(books);
}

// src/routes/books/+page.js

import { error } from "@sveltejs/kit";
import { trpc } from "$lib/trpc";

export async function load({ fetch }) {
  /** @type {{ title: string; author: string; }[]} */
  const books = await trpc(fetch).books.all.query()
    .catch(() => {
      throw error(500, "Failed to fetch books");
    });

  return { books };
}

// src/wherever/you/keep/trpc/routers/books.js

export const booksRouter = t.router({
  all: t.procedure.query(() => {
    const books = await db.query(/* query */);
    return books;
  }),
});

A downside to this approach is that you get yet another abstraction layer, and you have to set up tRPC.

Importance

would make my life easier

Additional Information

No response

@qurafi
Copy link

qurafi commented Apr 22, 2023

I think it's hard and tricky to implement as typescript need to know where each endpoint type at compile time, so it's not possible to resolve types just from the provided url, but we would probably need to generate a type map something like:

interface EndpointMap {
  "/api/books": typeof import("routes/api/books/+server"),
}

And then we could patch the fetch function type in load to retrieve types for the corresponding url(and method). This works great for simple api urls but does not handle dynamic urls.

Also we should note that the Response interface used by fetch and svelte endpoints are not strictly typed but we could easily patch it:

interface TypedResponse<T = any> extends Response {
  json(): Promise<T>;
}

So in your endpoints you will use TypedResponse instead of Response

function typed_json<T>(x: T) {
  return json(x) as TypedResponse<T>;
}

type Book = { title: string; author: string };

export async function GET() {
  const data: Book[] = ...;
  return typed_json(data);
}

To extract types from TypedResponse you could use this helper types:

// TypedResponse<T> -> T
type GetResponseType<R> = Awaited<R> extends TypedResponse<infer T> ? T : never;

// infer type of endpoint function
type GetEndpointType<S extends RequestHandler> = GetResponseType<ReturnType<S>>

So now to get inferred types in load function you will simply cast the response of fetch:

export async function load({ fetch }: PageLoadEvent) {
	const books = (fetch('/api/books') as ReturnType<typeof import('../api/books/+server').GET>)
		.then((res) => res.json())
		.catch(() => {
			throw error(500, 'Failed to fetch books');
		});

	return { books };
}

@nounder
Copy link

nounder commented May 4, 2023

To expand on @mhmd-22 ideas, here is how SvelteKit do typing for load functions:
https://svelte.dev/blog/zero-config-type-safety

Current implementation:

export async function write_types(config, manifest_data, file) {

@eltigerchino
Copy link
Member

Closed as duplicate of #647

@eltigerchino eltigerchino closed this as not planned Won't fix, can't repro, duplicate, stale Nov 14, 2023
@AlbertMarashi
Copy link

#647 (comment)

@qurafi I came up with virtually the exact same idea independently, let me know what you think of my idea

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

6 participants