Skip to content

Commit

Permalink
feat: provide PageProps, LayoutProps types (#13308)
Browse files Browse the repository at this point in the history
Closes #12726

---------

Co-authored-by: Tee Ming <chewteeming01@gmail.com>
  • Loading branch information
rChaoz and eltigerchino authored Jan 15, 2025
1 parent fb04de2 commit b60707c
Show file tree
Hide file tree
Showing 8 changed files with 112 additions and 22 deletions.
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 @@ -162,7 +162,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

0 comments on commit b60707c

Please sign in to comment.