Skip to content

feat: provide PageProps, LayoutProps types #13308

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

Merged
merged 6 commits into from
Jan 15, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/ten-onions-talk.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@sveltejs/kit': minor
---

feat: provide `PageProps` and `LayoutProps` types
34 changes: 28 additions & 6 deletions documentation/docs/20-core-concepts/10-routing.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ Pages can receive data from `load` functions via the `data` prop.
```svelte
<!--- file: src/routes/blog/[slug]/+page.svelte --->
<script>
/** @type {{ data: import('./$types').PageData }} */
/** @type {import('./$types').PageProps} */
let { data } = $props();
</script>

Expand All @@ -51,7 +51,9 @@ Pages can receive data from `load` functions via the `data` prop.
```

> [!LEGACY]
> In Svelte 4, you'd use `export let data` instead
> `PageProps` was added in 2.16.0. In earlier versions, you had to type the `data` property manually with `PageData` instead, see [$types](#\$types).
>
> In Svelte 4, you'd use `export let data` instead.

> [!NOTE] SvelteKit uses `<a>` elements to navigate between routes, rather than a framework-specific `<Link>` component.

Expand Down Expand Up @@ -212,7 +214,7 @@ We can create a layout that only applies to pages below `/settings` (while inher
```svelte
<!--- file: src/routes/settings/+layout.svelte --->
<script>
/** @type {{ data: import('./$types').LayoutData, children: import('svelte').Snippet }} */
/** @type {import('./$types').LayoutProps} */
let { data, children } = $props();
</script>

Expand All @@ -227,6 +229,9 @@ We can create a layout that only applies to pages below `/settings` (while inher
{@render children()}
```

> [!LEGACY]
> `LayoutProps` was added in 2.16.0. In earlier versions, you had to [type the properties manually instead](#\$types).

You can see how `data` is populated by looking at the `+layout.js` example in the next section just below.

By default, each layout inherits the layout above it. Sometimes that isn't what you want - in this case, [advanced layouts](advanced-routing#Advanced-layouts) can help you.
Expand Down Expand Up @@ -255,7 +260,7 @@ Data returned from a layout's `load` function is also available to all its child
```svelte
<!--- file: src/routes/settings/profile/+page.svelte --->
<script>
/** @type {{ data: import('./$types').PageData }} */
/** @type {import('./$types').PageProps} */
let { data } = $props();

console.log(data.sections); // [{ slug: 'profile', title: 'Profile' }, ...]
Expand Down Expand Up @@ -388,16 +393,33 @@ export async function fallback({ request }) {

Throughout the examples above, we've been importing types from a `$types.d.ts` file. This is a file SvelteKit creates for you in a hidden directory if you're using TypeScript (or JavaScript with JSDoc type annotations) to give you type safety when working with your root files.

For example, annotating `let { data } = $props()` with `PageData` (or `LayoutData`, for a `+layout.svelte` file) tells TypeScript that the type of `data` is whatever was returned from `load`:
For example, annotating `let { data } = $props()` with `PageProps` (or `LayoutProps`, for a `+layout.svelte` file) tells TypeScript that the type of `data` is whatever was returned from `load`:

```svelte
<!--- file: src/routes/blog/[slug]/+page.svelte --->
<script>
/** @type {{ data: import('./$types').PageData }} */
/** @type {import('./$types').PageProps} */
let { data } = $props();
</script>
```

> [!NOTE]
> The `PageProps` and `LayoutProps` types, added in 2.16.0, are a shortcut for typing the `data` prop as `PageData` or `LayoutData`, as well as other props, such as `form` for pages, or `children` for layouts. In earlier versions, you had to type these properties manually. For example, for a page:
>
> ```js
> /// file: +page.svelte
> /** @type {{ data: import('./$types').PageData, form: import('./$types').ActionData }} */
> let { data, form } = $props();
> ```
>
> Or, for a layout:
>
> ```js
> /// file: +layout.svelte
> /** @type {{ data: import('./$types').LayoutData, children: Snippet }} */
> let { data, children } = $props();
> ```

In turn, annotating the `load` function with `PageLoad`, `PageServerLoad`, `LayoutLoad` or `LayoutServerLoad` (for `+page.js`, `+page.server.js`, `+layout.js` and `+layout.server.js` respectively) ensures that `params` and the return value are correctly typed.

If you're using VS Code or any IDE that supports the language server protocol and TypeScript plugins then you can omit these types _entirely_! Svelte's IDE tooling will insert the correct types for you, so you'll get type checking without writing them yourself. It also works with our command line tool `svelte-check`.
Expand Down
29 changes: 22 additions & 7 deletions documentation/docs/20-core-concepts/20-load.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ export function load({ params }) {
```svelte
<!--- file: src/routes/blog/[slug]/+page.svelte --->
<script>
/** @type {{ data: import('./$types').PageData }} */
/** @type {import('./$types').PageProps} */
let { data } = $props();
</script>

Expand All @@ -33,7 +33,14 @@ export function load({ params }) {
```

> [!LEGACY]
> In Svelte 4, you'd use `export let data` instead
> Before version 2.16.0, the props of a page and layout had to be typed individually:
> ```js
> /// file: +page.svelte
> /** @type {{ data: import('./$types').PageData }} */
> let { data } = $props();
> ```
>
> In Svelte 4, you'd use `export let data` instead.

Thanks to the generated `$types` module, we get full type safety.

Expand Down Expand Up @@ -88,7 +95,7 @@ export async function load() {
```svelte
<!--- file: src/routes/blog/[slug]/+layout.svelte --->
<script>
/** @type {{ data: import('./$types').LayoutData, children: Snippet }} */
/** @type {import('./$types').LayoutProps} */
let { data, children } = $props();
</script>

Expand All @@ -111,14 +118,22 @@ export async function load() {
</aside>
```

> [!LEGACY]
> `LayoutProps` was added in 2.16.0. In earlier versions, properties had to be typed individually:
> ```js
> /// file: +layout.svelte
> /** @type {{ data: import('./$types').LayoutData, children: Snippet }} */
> let { data, children } = $props();
> ```

Data returned from layout `load` functions is available to child `+layout.svelte` components and the `+page.svelte` component as well as the layout that it 'belongs' to.

```svelte
/// file: src/routes/blog/[slug]/+page.svelte
<script>
+++import { page } from '$app/state';+++

/** @type {{ data: import('./$types').PageData }} */
/** @type {import('./$types').PageProps} */
let { data } = $props();

+++ // we can access `data.posts` because it's returned from
Expand Down Expand Up @@ -372,7 +387,7 @@ export async function load({ parent }) {
```svelte
<!--- file: src/routes/abc/+page.svelte --->
<script>
/** @type {{ data: import('./$types').PageData }} */
/** @type {import('./$types').PageProps} */
let { data } = $props();
</script>

Expand Down Expand Up @@ -511,7 +526,7 @@ This is useful for creating skeleton loading states, for example:
```svelte
<!--- file: src/routes/blog/[slug]/+page.svelte --->
<script>
/** @type {{ data: import('./$types').PageData }} */
/** @type {import('./$types').PageProps} */
let { data } = $props();
</script>

Expand Down Expand Up @@ -652,7 +667,7 @@ export async function load({ fetch, depends }) {
<script>
import { invalidate, invalidateAll } from '$app/navigation';

/** @type {{ data: import('./$types').PageData }} */
/** @type {import('./$types').PageProps} */
let { data } = $props();

function rerunLoadFunction() {
Expand Down
17 changes: 12 additions & 5 deletions documentation/docs/20-core-concepts/30-form-actions.md
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,7 @@ export const actions = {
```svelte
<!--- file: src/routes/login/+page.svelte --->
<script>
/** @type {{ data: import('./$types').PageData, form: import('./$types').ActionData }} */
/** @type {import('./$types').PageProps} */
let { data, form } = $props();
</script>

Expand All @@ -152,7 +152,14 @@ export const actions = {
```

> [!LEGACY]
> In Svelte 4, you'd use `export let data` and `export let form` instead to declare properties
> `PageProps` was added in 2.16.0. In earlier versions, you had to type the `data` and `form` properties individually:
> ```js
> /// file: +page.svelte
> /** @type {{ data: import('./$types').PageData, form: import('./$types').ActionData }} */
> let { data, form } = $props();
> ```
>
> In Svelte 4, you'd use `export let data` and `export let form` instead to declare properties.

### Validation errors

Expand Down Expand Up @@ -339,7 +346,7 @@ The easiest way to progressively enhance a form is to add the `use:enhance` acti
<script>
+++import { enhance } from '$app/forms';+++

/** @type {{ form: import('./$types').ActionData }} */
/** @type {import('./$types').PageProps} */
let { form } = $props();
</script>

Expand Down Expand Up @@ -390,7 +397,7 @@ If you return a callback, you may need to reproduce part of the default `use:enh
<script>
import { enhance, +++applyAction+++ } from '$app/forms';

/** @type {{ form: import('./$types').ActionData }} */
/** @type {import('./$types').PageProps} */
let { form } = $props();
</script>

Expand Down Expand Up @@ -427,7 +434,7 @@ We can also implement progressive enhancement ourselves, without `use:enhance`,
import { invalidateAll, goto } from '$app/navigation';
import { applyAction, deserialize } from '$app/forms';

/** @type {{ form: import('./$types').ActionData }} */
/** @type {import('./$types').PageProps} */
let { form } = $props();

/** @param {SubmitEvent & { currentTarget: EventTarget & HTMLFormElement}} event */
Expand Down
6 changes: 3 additions & 3 deletions documentation/docs/20-core-concepts/50-state-management.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ You might wonder how we're able to use `page.data` and other [app state]($app-st
<script>
import { setContext } from 'svelte';

/** @type {{ data: import('./$types').LayoutData }} */
/** @type {import('./$types').LayoutProps} */
let { data } = $props();

// Pass a function referencing our state
Expand Down Expand Up @@ -126,7 +126,7 @@ When you navigate around your application, SvelteKit reuses existing layout and
```svelte
<!--- file: src/routes/blog/[slug]/+page.svelte --->
<script>
/** @type {{ data: import('./$types').PageData }} */
/** @type {import('./$types').PageProps} */
let { data } = $props();

// THIS CODE IS BUGGY!
Expand All @@ -149,7 +149,7 @@ Instead, we need to make the value [_reactive_](/tutorial/svelte/state):
```svelte
/// file: src/routes/blog/[slug]/+page.svelte
<script>
/** @type {{ data: import('./$types').PageData }} */
/** @type {import('./$types').PageProps} */
let { data } = $props();

+++ let wordCount = $derived(data.content.split(' ').length);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,7 @@ export function load() {
```svelte
<!--- file: +layout.svelte --->
<script>
/** @type {{ data: import('./$types').LayoutServerData }} */
/** @type {import('./$types').LayoutProps} */
let { data } = $props();
</script>

Expand Down
31 changes: 31 additions & 0 deletions documentation/docs/98-reference/54-types.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,37 @@ export async function load({ params, fetch }) {
}
```

The return types of the load functions are then available through the `$types` module as `PageData` and `LayoutData` respectively, while the union of the return values of all `Actions` is available as `ActionData`. Starting with version 2.16.0, two additional helper types are provided. `PageProps` defines `data: PageData`, as well as `form: ActionData`, when there are actions defined. `LayoutProps` defines `data: LayoutData`, as well as `children: Snippet`:

```svelte
<!--- file: src/routes/+page.svelte --->
<script>
/** @type {import('./$types').PageProps} */
let { data, form } = $props();
</script>
```

> [!LEGACY]
> Before 2.16.0:
> ```svelte
> <!--- file: src/routes/+page.svelte --->
> <script>
> /** @type {{ data: import('./$types').PageData, form: import('./$types').ActionData }} */
> let { data, form } = $props();
> </script>
> ```
>
> Using Svelte 4:
> ```svelte
> <!--- file: src/routes/+page.svelte --->
> <script>
> /** @type {import('./$types').PageData} */
> export let data;
> /** @type {import('./$types').ActionData} */
> export let form;
> </script>
> ```

> [!NOTE] For this to work, your own `tsconfig.json` or `jsconfig.json` should extend from the generated `.svelte-kit/tsconfig.json` (where `.svelte-kit` is your [`outDir`](configuration#outDir)):
>
> `{ "extends": "./.svelte-kit/tsconfig.json" }`
Expand Down
10 changes: 10 additions & 0 deletions packages/kit/src/core/sync/write_types/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -270,6 +270,12 @@ function update_types(config, routes, route, to_delete = new Set()) {
'export type Actions<OutputData extends Record<string, any> | void = Record<string, any> | void> = Kit.Actions<RouteParams, OutputData, RouteId>'
);
}

if (route.leaf.server) {
exports.push('export type PageProps = { data: PageData; form: ActionData }');
} else {
exports.push('export type PageProps = { data: PageData }');
}
}

if (route.layout) {
Expand Down Expand Up @@ -333,6 +339,10 @@ function update_types(config, routes, route, to_delete = new Set()) {

if (proxies.server?.modified) to_delete.delete(proxies.server.file_name);
if (proxies.universal?.modified) to_delete.delete(proxies.universal.file_name);

exports.push(
'export type LayoutProps = { data: LayoutData; children: import("svelte").Snippet }'
);
}

if (route.endpoint) {
Expand Down
Loading