diff --git a/.changeset/ninety-suns-relax.md b/.changeset/ninety-suns-relax.md new file mode 100644 index 000000000000..29dd3e693b5c --- /dev/null +++ b/.changeset/ninety-suns-relax.md @@ -0,0 +1,6 @@ +--- +'create-svelte': patch +'@sveltejs/kit': patch +--- + +Add App namespace for app-level types diff --git a/documentation/docs/01-routing.md b/documentation/docs/01-routing.md index 8af291a249b8..8dc0e1364c9b 100644 --- a/documentation/docs/01-routing.md +++ b/documentation/docs/01-routing.md @@ -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, Platform = Record> { +export interface RequestEvent { request: Request; url: URL; params: Record; - locals: Locals; - platform: Platform; + locals: App.Locals; + platform: App.Platform; } type Body = JSONString | Uint8Array | ReadableStream | stream.Readable; @@ -71,17 +71,13 @@ interface Fallthrough { fallthrough: true; } -export interface RequestHandler< - Locals = Record, - Platform = Record, - Output extends Body = Body -> { - (event: RequestEvent): MaybePromise< - Either, Fallthrough> - >; +export interface RequestHandler { + (event: RequestEvent): MaybePromise, 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 diff --git a/documentation/docs/02-layouts.md b/documentation/docs/02-layouts.md index 6211e825b3d1..9174dc651a9b 100644 --- a/documentation/docs/02-layouts.md +++ b/documentation/docs/02-layouts.md @@ -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 = Record, - Stuff extends Record = Record, - Session = any -> extends LoadInput { +export interface ErrorLoadInput = Record> + extends LoadInput { status?: number; error?: Error; } diff --git a/documentation/docs/03-loading.md b/documentation/docs/03-loading.md index 51d91571432b..1caa408b67ba 100644 --- a/documentation/docs/03-loading.md +++ b/documentation/docs/03-loading.md @@ -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 = Record, - Stuff extends Record = Record, - Session = any -> { +export interface LoadInput = Record> { url: URL; - params: PageParams; + params: Params; fetch(info: RequestInfo, init?: RequestInit): Promise; - session: Session; - stuff: Stuff; + session: App.Session; + stuff: Partial; } -export interface LoadOutput< - Props extends Record = Record, - Stuff extends Record = Record -> { +export interface LoadOutput = Record> { status?: number; error?: string | Error; redirect?: string; props?: Props; - stuff?: Stuff; + stuff?: Partial; maxage?: number; } -interface LoadInputExtends { - stuff?: Record; - pageParams?: Record; - session?: any; -} -interface LoadOutputExtends { - stuff?: Record; - props?: Record; -} - type MaybePromise = T | Promise; interface Fallthrough { fallthrough: true; } -export interface Load< - Input extends LoadInputExtends = Required, - Output extends LoadOutputExtends = Required -> { - ( - input: LoadInput< - InferValue>, - InferValue>, - InferValue - > - ): MaybePromise< - Either< - Fallthrough, - LoadOutput< - InferValue>, - InferValue> - > - > - >; + +export interface Load, Props = Record> { + (input: LoadInput): MaybePromise>>; } ``` +> 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 diff --git a/documentation/docs/04-hooks.md b/documentation/docs/04-hooks.md index f3378de7d5fc..c873dd535bb6 100644 --- a/documentation/docs/04-hooks.md +++ b/documentation/docs/04-hooks.md @@ -22,26 +22,28 @@ If unimplemented, defaults to `({ event, resolve }) => resolve(event)`. // everything else must be a type of string type ResponseHeaders = Record; -export interface RequestEvent, Platform = Record> { +export interface RequestEvent { request: Request; url: URL; params: Record; - locals: Locals; - platform: Platform; + locals: App.Locals; + platform: App.Platform; } export interface ResolveOpts { ssr?: boolean; } -export interface Handle, Platform = Record> { +export interface Handle { (input: { - event: RequestEvent; - resolve(event: RequestEvent, opts?: ResolveOpts): MaybePromise; + event: RequestEvent; + resolve(event: RequestEvent, opts?: ResolveOpts): MaybePromise; }): MaybePromise; } ``` +> 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 @@ -85,8 +87,8 @@ If unimplemented, SvelteKit will log the error with default formatting. ```ts // Declaration types for handleError hook -export interface HandleError, Platform = Record> { - (input: { error: Error & { frame?: string }; event: RequestEvent }): void; +export interface HandleError { + (input: { error: Error & { frame?: string }; event: RequestEvent }): void; } ``` @@ -108,12 +110,8 @@ If unimplemented, session is `{}`. ```ts // Declaration types for getSession hook -export interface GetSession< - Locals = Record, - Platform = Record, - Session = any -> { - (event: RequestEvent): MaybePromise; +export interface GetSession { + (event: RequestEvent): MaybePromise; } ``` @@ -130,7 +128,7 @@ export function getSession(event) { email: event.locals.user.email, avatar: event.locals.user.avatar } - } + } : {}; } ``` diff --git a/documentation/docs/15-typescript.md b/documentation/docs/15-typescript.md new file mode 100644 index 000000000000..7722933b0f06 --- /dev/null +++ b/documentation/docs/15-typescript.md @@ -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 +/// + +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). diff --git a/packages/adapter-cloudflare/README.md b/packages/adapter-cloudflare/README.md index dffc3709a003..9a52fc120c4b 100644 --- a/packages/adapter-cloudflare/README.md +++ b/packages/adapter-cloudflare/README.md @@ -56,16 +56,25 @@ When configuring your project settings, you must use the following settings: The [`env`](https://developers.cloudflare.com/workers/runtime-apis/fetch-event#parameters) object, containing KV namespaces etc, is passed to SvelteKit via the `platform` property, meaning you can access it in hooks and endpoints: -```ts -interface Locals {} +```diff +// src/app.d.ts +declare namespace App { + interface Locals {} -interface Platform { - env: { - COUNTER: DurableObjectNamespace; - }; ++ interface Platform { ++ env: { ++ COUNTER: DurableObjectNamespace; ++ }; ++ } + + interface Session {} + + interface Stuff {} } +``` -export async function post({ request, platform }) { +```js +export async function post({ request, platform }) { const counter = platform.env.COUNTER.idFromName('A'); } ``` diff --git a/packages/create-svelte/templates/default/src/app.d.ts b/packages/create-svelte/templates/default/src/app.d.ts new file mode 100644 index 000000000000..dd7d35856f42 --- /dev/null +++ b/packages/create-svelte/templates/default/src/app.d.ts @@ -0,0 +1,15 @@ +/// + +// See https://kit.svelte.dev/docs#typescript +// for information about these interfaces +declare namespace App { + interface Locals { + userid: string; + } + + interface Platform {} + + interface Session {} + + interface Stuff {} +} diff --git a/packages/create-svelte/templates/default/src/global.d.ts b/packages/create-svelte/templates/default/src/global.d.ts deleted file mode 100644 index 63908c66cfd4..000000000000 --- a/packages/create-svelte/templates/default/src/global.d.ts +++ /dev/null @@ -1 +0,0 @@ -/// diff --git a/packages/create-svelte/templates/default/src/lib/types.d.ts b/packages/create-svelte/templates/default/src/lib/types.d.ts deleted file mode 100644 index 6edddd1d6fb6..000000000000 --- a/packages/create-svelte/templates/default/src/lib/types.d.ts +++ /dev/null @@ -1,7 +0,0 @@ -/** - * Can be made globally available by placing this - * inside `global.d.ts` and removing `export` keyword - */ -export interface Locals { - userid: string; -} diff --git a/packages/create-svelte/templates/default/src/routes/todos/[uid].json.ts b/packages/create-svelte/templates/default/src/routes/todos/[uid].json.ts index 0ca276dbcbf3..e86c5f9bfeb8 100644 --- a/packages/create-svelte/templates/default/src/routes/todos/[uid].json.ts +++ b/packages/create-svelte/templates/default/src/routes/todos/[uid].json.ts @@ -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 = async (event) => { +export const patch: RequestHandler = async (event) => { const data = await event.request.formData(); return api(event, `todos/${event.locals.userid}/${event.params.uid}`, { @@ -13,6 +12,6 @@ export const patch: RequestHandler = async (event) => { }; // DELETE /todos/:uid.json -export const del: RequestHandler = async (event) => { +export const del: RequestHandler = async (event) => { return api(event, `todos/${event.locals.userid}/${event.params.uid}`); }; diff --git a/packages/create-svelte/templates/default/src/routes/todos/_api.ts b/packages/create-svelte/templates/default/src/routes/todos/_api.ts index 4e13eb029573..97e8f790050d 100644 --- a/packages/create-svelte/templates/default/src/routes/todos/_api.ts +++ b/packages/create-svelte/templates/default/src/routes/todos/_api.ts @@ -1,5 +1,4 @@ import type { EndpointOutput, RequestEvent } from '@sveltejs/kit'; -import type { Locals } from '$lib/types'; /* This module is used by the /todos.json and /todos/[uid].json @@ -15,7 +14,7 @@ import type { Locals } from '$lib/types'; const base = 'https://api.svelte.dev'; export async function api( - event: RequestEvent, + event: RequestEvent, resource: string, data?: Record ): Promise { diff --git a/packages/create-svelte/templates/default/src/routes/todos/index.json.ts b/packages/create-svelte/templates/default/src/routes/todos/index.json.ts index c300fd971491..1edb0058a9cc 100644 --- a/packages/create-svelte/templates/default/src/routes/todos/index.json.ts +++ b/packages/create-svelte/templates/default/src/routes/todos/index.json.ts @@ -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 = 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}`); @@ -17,7 +16,7 @@ export const get: RequestHandler = async (event) => { }; // POST /todos.json -export const post: RequestHandler = async (event) => { +export const post: RequestHandler = async (event) => { const data = await event.request.formData(); const response = await api(event, `todos/${event.locals.userid}`, { diff --git a/packages/create-svelte/templates/skeleton/src/app.d.ts b/packages/create-svelte/templates/skeleton/src/app.d.ts new file mode 100644 index 000000000000..161703cac14d --- /dev/null +++ b/packages/create-svelte/templates/skeleton/src/app.d.ts @@ -0,0 +1,13 @@ +/// + +// See https://kit.svelte.dev/docs#typescript +// for information about these interfaces +declare namespace App { + interface Locals {} + + interface Platform {} + + interface Session {} + + interface Stuff {} +} diff --git a/packages/create-svelte/templates/skeleton/src/global.d.ts b/packages/create-svelte/templates/skeleton/src/global.d.ts deleted file mode 100644 index 63908c66cfd4..000000000000 --- a/packages/create-svelte/templates/skeleton/src/global.d.ts +++ /dev/null @@ -1 +0,0 @@ -/// diff --git a/packages/kit/.eslintrc.json b/packages/kit/.eslintrc.json index c8debbb5c06e..bca2ace00477 100644 --- a/packages/kit/.eslintrc.json +++ b/packages/kit/.eslintrc.json @@ -11,5 +11,8 @@ "prefetchRoutes": true, "beforeNavigate": true, "afterNavigate": true + }, + "rules": { + "@typescript-eslint/no-empty-interface": "off" } } diff --git a/packages/kit/src/core/dev/plugin.js b/packages/kit/src/core/dev/plugin.js index a35314d02f17..8a676a9c3371 100644 --- a/packages/kit/src/core/dev/plugin.js +++ b/packages/kit/src/core/dev/plugin.js @@ -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: diff --git a/packages/kit/src/runtime/server/index.js b/packages/kit/src/runtime/server/index.js index 3f1edbd16583..d5b56d2a009a 100644 --- a/packages/kit/src/runtime/server/index.js +++ b/packages/kit/src/runtime/server/index.js @@ -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 }; diff --git a/packages/kit/test/apps/basics/src/app.d.ts b/packages/kit/test/apps/basics/src/app.d.ts new file mode 100644 index 000000000000..eada2bd808ce --- /dev/null +++ b/packages/kit/test/apps/basics/src/app.d.ts @@ -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; + } +} diff --git a/packages/kit/test/apps/basics/src/hooks.js b/packages/kit/test/apps/basics/src/hooks.js index dec1a29338cb..dfb9959d0e66 100644 --- a/packages/kit/test/apps/basics/src/hooks.js +++ b/packages/kit/test/apps/basics/src/hooks.js @@ -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} */ diff --git a/packages/kit/test/apps/basics/src/routes/store/index.svelte b/packages/kit/test/apps/basics/src/routes/store/index.svelte index 43eb478eb39f..047138cbad77 100644 --- a/packages/kit/test/apps/basics/src/routes/store/index.svelte +++ b/packages/kit/test/apps/basics/src/routes/store/index.svelte @@ -7,12 +7,18 @@ let calls = 0; onMount(() => { - session.set(calls); + session.update(($session) => ({ + ...$session, + calls + })); }); const unsubscribe = page.subscribe(() => { calls++; - session.set(calls); + session.update(($session) => ({ + ...$session, + calls + })); }); onDestroy(unsubscribe); diff --git a/packages/kit/test/apps/basics/src/routes/store/result.svelte b/packages/kit/test/apps/basics/src/routes/store/result.svelte index f92ae29651e8..0adaf8f06c93 100644 --- a/packages/kit/test/apps/basics/src/routes/store/result.svelte +++ b/packages/kit/test/apps/basics/src/routes/store/result.svelte @@ -8,9 +8,9 @@ let calls = 0; onMount(() => { - calls = get(session); + ({ calls } = get(session)); });

Result

-

Calls: {calls}

\ No newline at end of file +

Calls: {calls}

diff --git a/packages/kit/types/ambient-modules.d.ts b/packages/kit/types/ambient-modules.d.ts index 264b9780ba28..8d9643120711 100644 --- a/packages/kit/types/ambient-modules.d.ts +++ b/packages/kit/types/ambient-modules.d.ts @@ -1,5 +1,12 @@ /* eslint-disable import/no-duplicates */ +declare namespace App { + interface Locals {} + interface Platform {} + interface Session {} + interface Stuff {} +} + declare module '$app/env' { /** * Whether or not app is in AMP mode. @@ -106,10 +113,10 @@ declare module '$app/stores' { * A convenience function around `getContext` that returns `{ navigating, page, session }`. * Most of the time, you won't need to use it. */ - export function getStores(): { + export function getStores(): { navigating: typeof navigating; page: typeof page; - session: Writable; + session: Writable; updated: typeof updated; }; /** @@ -132,7 +139,7 @@ declare module '$app/stores' { * A writable store whose initial value is whatever was returned from `getSession`. * It can be written to, but this will not cause changes to persist on the server — this is something you must implement yourself. */ - export const session: Writable; + export const session: Writable; /** * A writable store indicating if the site was updated since the store was created. * It can be written to when custom logic is required to detect updates. diff --git a/packages/kit/types/endpoint.d.ts b/packages/kit/types/endpoint.d.ts index d48099433d00..d60f39b8a7db 100644 --- a/packages/kit/types/endpoint.d.ts +++ b/packages/kit/types/endpoint.d.ts @@ -13,12 +13,6 @@ export interface Fallthrough { fallthrough: true; } -export interface RequestHandler< - Locals = Record, - Platform = Record, - Output extends Body = Body -> { - (event: RequestEvent): MaybePromise< - Either, Fallthrough> - >; +export interface RequestHandler { + (event: RequestEvent): MaybePromise, Fallthrough>>; } diff --git a/packages/kit/types/helper.d.ts b/packages/kit/types/helper.d.ts index 3e85e1bf187c..f48f72b57491 100644 --- a/packages/kit/types/helper.d.ts +++ b/packages/kit/types/helper.d.ts @@ -16,9 +16,6 @@ export type ResponseHeaders = Record; type Only = { [P in keyof T]: T[P] } & { [P in Exclude]?: never }; export type Either = Only | Only; -export type InferValue = T extends Record - ? Val - : Default; export type MaybePromise = T | Promise; export type RecursiveRequired = { // Recursive implementation of TypeScript's Required utility type. diff --git a/packages/kit/types/hooks.d.ts b/packages/kit/types/hooks.d.ts index afaca61969c3..fdcf697bdfa2 100644 --- a/packages/kit/types/hooks.d.ts +++ b/packages/kit/types/hooks.d.ts @@ -2,35 +2,31 @@ import { MaybePromise } from './helper'; export type StrictBody = string | Uint8Array; -export interface RequestEvent, Platform = Record> { +export interface RequestEvent { request: Request; url: URL; params: Record; - locals: Locals; - platform: Readonly; + locals: App.Locals; + platform: Readonly; } -export interface GetSession< - Locals = Record, - Platform = Record, - Session = any -> { - (event: RequestEvent): MaybePromise; +export interface GetSession { + (event: RequestEvent): MaybePromise; } export interface ResolveOpts { ssr?: boolean; } -export interface Handle, Platform = Record> { +export interface Handle { (input: { - event: RequestEvent; - resolve(event: RequestEvent, opts?: ResolveOpts): MaybePromise; + event: RequestEvent; + resolve(event: RequestEvent, opts?: ResolveOpts): MaybePromise; }): MaybePromise; } -export interface HandleError> { - (input: { error: Error & { frame?: string }; event: RequestEvent }): void; +export interface HandleError { + (input: { error: Error & { frame?: string }; event: RequestEvent }): void; } export interface ExternalFetch { diff --git a/packages/kit/types/page.d.ts b/packages/kit/types/page.d.ts index 5e143862cf33..8aff51ce9183 100644 --- a/packages/kit/types/page.d.ts +++ b/packages/kit/types/page.d.ts @@ -1,85 +1,32 @@ import { Fallthrough } from './endpoint'; -import { Either, InferValue, MaybePromise } from './helper'; +import { Either, MaybePromise } from './helper'; -export interface LoadInput< - PageParams extends Record = Record, - Stuff extends Record = Record, - Session = any -> { +export interface LoadInput> { url: URL; - params: PageParams; + params: Params; fetch(info: RequestInfo, init?: RequestInit): Promise; - session: Session; - stuff: Stuff; + session: App.Session; + stuff: Partial; } -export interface ErrorLoadInput< - PageParams extends Record = Record, - Stuff extends Record = Record, - Session = any -> extends LoadInput { +export interface ErrorLoadInput> extends LoadInput { status?: number; error?: Error; } -export interface LoadOutput< - Props extends Record = Record, - Stuff extends Record = Record -> { +export interface LoadOutput> { status?: number; error?: string | Error; redirect?: string; props?: Props; - stuff?: Stuff; + stuff?: Partial; maxage?: number; } -interface LoadInputExtends { - stuff?: Record; - pageParams?: Record; - session?: any; +export interface Load, Props = Record> { + (input: LoadInput): MaybePromise>>; } -interface LoadOutputExtends { - stuff?: Record; - props?: Record; -} - -export interface Load< - Input extends LoadInputExtends = Required, - Output extends LoadOutputExtends = Required -> { - ( - input: LoadInput< - InferValue>, - InferValue>, - InferValue - > - ): MaybePromise< - Either< - Fallthrough, - LoadOutput< - InferValue>, - InferValue> - > - > - >; -} - -export interface ErrorLoad< - Input extends LoadInputExtends = Required, - Output extends LoadOutputExtends = Required -> { - ( - input: ErrorLoadInput< - InferValue>, - InferValue>, - InferValue - > - ): MaybePromise< - LoadOutput< - InferValue>, - InferValue> - > - >; +export interface ErrorLoad, Props = Record> { + (input: ErrorLoadInput): MaybePromise>; }