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

add a SvelteKit namespace for app-level types (#3569) #3670

Merged
merged 14 commits into from
Feb 2, 2022
6 changes: 6 additions & 0 deletions .changeset/ninety-suns-relax.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'create-svelte': patch
'@sveltejs/kit': patch
---

Add App namespace for app-level types
18 changes: 7 additions & 11 deletions documentation/docs/01-routing.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,12 +51,12 @@ Endpoints are modules written in `.js` (or `.ts`) files that export functions co
// Declaration types for Endpoints
// * declarations that are not exported are for internal use

export interface RequestEvent<Locals = Record<string, any>, Platform = Record<string, any>> {
export interface RequestEvent {
request: Request;
url: URL;
params: Record<string, string>;
locals: Locals;
platform: Platform;
locals: App.Locals;
platform: App.Platform;
}

type Body = JSONString | Uint8Array | ReadableStream | stream.Readable;
Expand All @@ -71,17 +71,13 @@ interface Fallthrough {
fallthrough: true;
}

export interface RequestHandler<
Locals = Record<string, any>,
Platform = Record<string, any>,
Output extends Body = Body
> {
(event: RequestEvent<Locals, Platform>): MaybePromise<
Either<Response | EndpointOutput<Output>, Fallthrough>
>;
export interface RequestHandler<Output extends Body = Body> {
(event: RequestEvent): MaybePromise<Either<Response | EndpointOutput<Output>, Fallthrough>>;
}
```

> See the [TypeScript](#typescript) section for information on `App.Locals` and `App.Platform`.

For example, our hypothetical blog page, `/blog/cool-article`, might request data from `/blog/cool-article.json`, which could be represented by a `src/routes/blog/[slug].json.js` endpoint:

```js
Expand Down
7 changes: 2 additions & 5 deletions documentation/docs/02-layouts.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,11 +80,8 @@ For example, if `src/routes/settings/notifications/index.svelte` failed to load,
// declaration type
// * also see type for `LoadOutput` in the Loading section

export interface ErrorLoadInput<
PageParams extends Record<string, string> = Record<string, string>,
Stuff extends Record<string, any> = Record<string, any>,
Session = any
> extends LoadInput<PageParams, Stuff, Session> {
export interface ErrorLoadInput<Params extends Record<string, string> = Record<string, string>>
extends LoadInput<Params> {
status?: number;
error?: Error;
}
Expand Down
53 changes: 11 additions & 42 deletions documentation/docs/03-loading.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,66 +8,35 @@ A component that defines a page or a layout can export a `load` function that ru
// Declaration types for Loading
// * declarations that are not exported are for internal use

export interface LoadInput<
PageParams extends Record<string, string> = Record<string, string>,
Stuff extends Record<string, any> = Record<string, any>,
Session = any
> {
export interface LoadInput<Params extends Record<string, string> = Record<string, string>> {
url: URL;
params: PageParams;
params: Params;
fetch(info: RequestInfo, init?: RequestInit): Promise<Response>;
session: Session;
stuff: Stuff;
session: App.Session;
stuff: Partial<App.Stuff>;
}

export interface LoadOutput<
Props extends Record<string, any> = Record<string, any>,
Stuff extends Record<string, any> = Record<string, any>
> {
export interface LoadOutput<Props extends Record<string, any> = Record<string, any>> {
status?: number;
error?: string | Error;
redirect?: string;
props?: Props;
stuff?: Stuff;
stuff?: Partial<App.Stuff>;
maxage?: number;
}

interface LoadInputExtends {
stuff?: Record<string, any>;
pageParams?: Record<string, string>;
session?: any;
}
interface LoadOutputExtends {
stuff?: Record<string, any>;
props?: Record<string, any>;
}

type MaybePromise<T> = T | Promise<T>;
interface Fallthrough {
fallthrough: true;
}
export interface Load<
Input extends LoadInputExtends = Required<LoadInputExtends>,
Output extends LoadOutputExtends = Required<LoadOutputExtends>
> {
(
input: LoadInput<
InferValue<Input, 'pageParams', Record<string, string>>,
InferValue<Input, 'stuff', Record<string, any>>,
InferValue<Input, 'session', any>
>
): MaybePromise<
Either<
Fallthrough,
LoadOutput<
InferValue<Output, 'props', Record<string, any>>,
InferValue<Output, 'stuff', Record<string, any>>
>
>
>;

export interface Load<Params = Record<string, string>, Props = Record<string, any>> {
(input: LoadInput<Params>): MaybePromise<Either<Fallthrough, LoadOutput<Props>>>;
}
```

> See the [TypeScript](#typescript) section for information on `App.Session` and `App.Stuff`.

Our example blog page might contain a `load` function like the following:

```html
Expand Down
28 changes: 13 additions & 15 deletions documentation/docs/04-hooks.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,26 +22,28 @@ If unimplemented, defaults to `({ event, resolve }) => resolve(event)`.
// everything else must be a type of string
type ResponseHeaders = Record<string, string | string[]>;

export interface RequestEvent<Locals = Record<string, any>, Platform = Record<string, any>> {
export interface RequestEvent {
request: Request;
url: URL;
params: Record<string, string>;
locals: Locals;
platform: Platform;
locals: App.Locals;
platform: App.Platform;
}

export interface ResolveOpts {
ssr?: boolean;
}

export interface Handle<Locals = Record<string, any>, Platform = Record<string, any>> {
export interface Handle {
(input: {
event: RequestEvent<Locals, Platform>;
resolve(event: RequestEvent<Locals, Platform>, opts?: ResolveOpts): MaybePromise<Response>;
event: RequestEvent;
resolve(event: RequestEvent, opts?: ResolveOpts): MaybePromise<Response>;
}): MaybePromise<Response>;
}
```

> See the [TypeScript](#typescript) section for information on `App.Locals` and `App.Platform`.

To add custom data to the request, which is passed to endpoints, populate the `event.locals` object, as shown below.

```js
Expand Down Expand Up @@ -85,8 +87,8 @@ If unimplemented, SvelteKit will log the error with default formatting.

```ts
// Declaration types for handleError hook
export interface HandleError<Locals = Record<string, any>, Platform = Record<string, any>> {
(input: { error: Error & { frame?: string }; event: RequestEvent<Locals, Platform> }): void;
export interface HandleError {
(input: { error: Error & { frame?: string }; event: RequestEvent }): void;
}
```

Expand All @@ -108,12 +110,8 @@ If unimplemented, session is `{}`.

```ts
// Declaration types for getSession hook
export interface GetSession<
Locals = Record<string, any>,
Platform = Record<string, any>,
Session = any
> {
(event: RequestEvent<Locals, Platform>): MaybePromise<Session>;
export interface GetSession {
(event: RequestEvent): MaybePromise<App.Session>;
}
```

Expand All @@ -130,7 +128,7 @@ export function getSession(event) {
email: event.locals.user.email,
avatar: event.locals.user.avatar
}
}
}
: {};
}
```
Expand Down
37 changes: 37 additions & 0 deletions documentation/docs/15-typescript.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
---
title: TypeScript
---

All APIs in SvelteKit are fully typed. Additionally, it's possible to tell SvelteKit how to type objects inside your app by declaring the `App` namespace. By default, a new project will have a file called `src/app.d.ts` containing the following:

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

declare namespace App {
interface Locals {}

interface Platform {}

interface Session {}

interface Stuff {}
}
```

By populating these interfaces, you will gain type safety when using `event.locals`, `event.platform`, `session` and `stuff`:

### App.Locals

The interface that defines `event.locals`, which can be accessed in [hooks](#hooks) (`handle`, `handleError` and `getSession`) and [endpoints](#endpoints).

### App.Platform

If your adapter provides [platform-specific context](#adapters-supported-environments-platform-specific-context) via `event.platform`, you can specify it here.

### App.Session

The interface that defines `session`, both as an argument to [`load`](#loading) functions and the value of the [session store](#modules-$app-stores).

### App.Stuff

The interface that defines `stuff`, as input or output to [`load`](#loading) or as the value of the `stuff` property of the [page store](#modules-$app-stores).
13 changes: 13 additions & 0 deletions packages/create-svelte/templates/default/src/app.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
/// <reference types="@sveltejs/kit" />

declare namespace App {
Rich-Harris marked this conversation as resolved.
Show resolved Hide resolved
interface Locals {
userid: string;
}

interface Platform {}

interface Session {}

interface Stuff {}
}
1 change: 0 additions & 1 deletion packages/create-svelte/templates/default/src/global.d.ts

This file was deleted.

7 changes: 0 additions & 7 deletions packages/create-svelte/templates/default/src/lib/types.d.ts

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
import { api } from './_api';
import type { RequestHandler } from '@sveltejs/kit';
import type { Locals } from '$lib/types';

// PATCH /todos/:uid.json
export const patch: RequestHandler<Locals> = async (event) => {
export const patch: RequestHandler = async (event) => {
const data = await event.request.formData();

return api(event, `todos/${event.locals.userid}/${event.params.uid}`, {
Expand All @@ -13,6 +12,6 @@ export const patch: RequestHandler<Locals> = async (event) => {
};

// DELETE /todos/:uid.json
export const del: RequestHandler<Locals> = async (event) => {
export const del: RequestHandler = async (event) => {
return api(event, `todos/${event.locals.userid}/${event.params.uid}`);
};
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
import { api } from './_api';
import type { RequestHandler } from '@sveltejs/kit';
import type { Locals } from '$lib/types';

// GET /todos.json
export const get: RequestHandler<Locals> = async (event) => {
export const get: RequestHandler = async (event) => {
// event.locals.userid comes from src/hooks.js
const response = await api(event, `todos/${event.locals.userid}`);

Expand All @@ -17,7 +16,7 @@ export const get: RequestHandler<Locals> = async (event) => {
};

// POST /todos.json
export const post: RequestHandler<Locals> = async (event) => {
export const post: RequestHandler = async (event) => {
const data = await event.request.formData();

const response = await api(event, `todos/${event.locals.userid}`, {
Expand Down
11 changes: 11 additions & 0 deletions packages/create-svelte/templates/skeleton/src/app.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
/// <reference types="@sveltejs/kit" />

declare namespace App {
Rich-Harris marked this conversation as resolved.
Show resolved Hide resolved
interface Locals {}

interface Platform {}

interface Session {}

interface Stuff {}
}
1 change: 0 additions & 1 deletion packages/create-svelte/templates/skeleton/src/global.d.ts

This file was deleted.

3 changes: 3 additions & 0 deletions packages/kit/.eslintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,5 +11,8 @@
"prefetchRoutes": true,
"beforeNavigate": true,
"afterNavigate": true
},
"rules": {
"@typescript-eslint/no-empty-interface": "off"
}
}
1 change: 1 addition & 0 deletions packages/kit/src/core/dev/plugin.js
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,7 @@ export async function create_plugin(config, cwd) {

/** @type {import('types/internal').Hooks} */
const hooks = {
// @ts-expect-error this picks up types that belong to the tests
getSession: user_hooks.getSession || (() => ({})),
handle: amp ? sequence(amp, handle) : handle,
handleError:
Expand Down
1 change: 1 addition & 0 deletions packages/kit/src/runtime/server/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ export async function respond(request, options, state = {}) {
request,
url,
params: {},
// @ts-expect-error this picks up types that belong to the tests
locals: {},
platform: state.platform
};
Expand Down
22 changes: 22 additions & 0 deletions packages/kit/test/apps/basics/src/app.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
declare namespace App {
interface Locals {
answer: number;
name: string;
}

interface Platform {}

interface Session {
answer: number;
calls: number;
}

interface Stuff {
message: string;
error: string;
page: string;
value: number;
x: string;
y: string;
}
}
5 changes: 4 additions & 1 deletion packages/kit/test/apps/basics/src/hooks.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,10 @@ import { sequence } from '../../../../src/hooks';

/** @type {import('@sveltejs/kit').GetSession} */
export function getSession(request) {
return request.locals;
return {
answer: request.locals.answer,
calls: 0
};
}

/** @type {import('@sveltejs/kit').HandleError} */
Expand Down
Loading