Skip to content

Commit 5a4dbe3

Browse files
authored
Infer route parameter type from matcher's guard check if applicable (#10755)
1 parent 6dd025c commit 5a4dbe3

File tree

10 files changed

+118
-9
lines changed

10 files changed

+118
-9
lines changed

.changeset/great-bags-heal.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@sveltejs/kit': patch
3+
---
4+
5+
feat: infer route parameter type from matcher's guard check if applicable

packages/kit/src/core/sync/write_types/index.js

Lines changed: 39 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -188,10 +188,18 @@ function update_types(config, routes, route, to_delete = new Set()) {
188188
// add 'Expand' helper
189189
// Makes sure a type is "repackaged" and therefore more readable
190190
declarations.push('type Expand<T> = T extends infer O ? { [K in keyof O]: O[K] } : never;');
191+
192+
// returns the predicate of a matcher's type guard - or string if there is no type guard
191193
declarations.push(
192-
`type RouteParams = { ${route.params
193-
.map((param) => `${param.name}${param.optional ? '?' : ''}: string`)
194-
.join('; ')} }`
194+
// TS complains on infer U, which seems weird, therefore ts-ignore it
195+
[
196+
'// @ts-ignore',
197+
'type MatcherParam<M> = M extends (param : string) => param is infer U ? U extends string ? U : string : string;'
198+
].join('\n')
199+
);
200+
201+
declarations.push(
202+
'type RouteParams = ' + generate_params_type(route.params, outdir, config) + ';'
195203
);
196204

197205
if (route.params.length > 0) {
@@ -265,7 +273,8 @@ function update_types(config, routes, route, to_delete = new Set()) {
265273

266274
if (route.layout) {
267275
let all_pages_have_load = true;
268-
const layout_params = new Set();
276+
/** @type {import('types').RouteParam[]} */
277+
const layout_params = [];
269278
const ids = ['RouteId'];
270279

271280
route.layout.child_pages?.forEach((page) => {
@@ -274,7 +283,9 @@ function update_types(config, routes, route, to_delete = new Set()) {
274283
if (leaf.route.page) ids.push(`"${leaf.route.id}"`);
275284

276285
for (const param of leaf.route.params) {
277-
layout_params.add(param.name);
286+
// skip if already added
287+
if (layout_params.some((p) => p.name === param.name)) continue;
288+
layout_params.push({ ...param, optional: true });
278289
}
279290

280291
ensureProxies(page, leaf.proxies);
@@ -301,9 +312,7 @@ function update_types(config, routes, route, to_delete = new Set()) {
301312
declarations.push(`type LayoutRouteId = ${ids.join(' | ')}`);
302313

303314
declarations.push(
304-
`type LayoutParams = RouteParams & { ${Array.from(layout_params).map(
305-
(param) => `${param}?: string`
306-
)} }`
315+
'type LayoutParams = RouteParams & ' + generate_params_type(layout_params, outdir, config)
307316
);
308317

309318
const {
@@ -567,6 +576,28 @@ function replace_ext_with_js(file_path) {
567576
return file_path.slice(0, -ext.length) + '.js';
568577
}
569578

579+
/**
580+
* @param {import('types').RouteParam[]} params
581+
* @param {string} outdir
582+
* @param {import('types').ValidatedConfig} config
583+
*/
584+
function generate_params_type(params, outdir, config) {
585+
/** @param {string} matcher */
586+
const path_to_matcher = (matcher) =>
587+
posixify(path.relative(outdir, path.join(config.kit.files.params, matcher)));
588+
589+
return `{ ${params
590+
.map(
591+
(param) =>
592+
`${param.name}${param.optional ? '?' : ''}: ${
593+
param.matcher
594+
? `MatcherParam<typeof import('${path_to_matcher(param.matcher)}').match>`
595+
: 'string'
596+
}`
597+
)
598+
.join('; ')} }`;
599+
}
600+
570601
/**
571602
* @param {string} content
572603
* @param {boolean} is_server

packages/kit/src/core/sync/write_types/index.spec.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ async function run_test(dir) {
1818
const initial = options({}, 'config');
1919

2020
initial.kit.files.assets = path.resolve(cwd, 'static');
21-
initial.kit.files.params = path.resolve(cwd, 'params');
21+
initial.kit.files.params = path.resolve(cwd, dir, 'params');
2222
initial.kit.files.routes = path.resolve(cwd, dir);
2323
initial.kit.outDir = path.resolve(cwd, path.join(dir, '.svelte-kit'));
2424

@@ -40,6 +40,7 @@ test('Creates correct $types', async () => {
4040
await run_test('layout-advanced');
4141
await run_test('slugs');
4242
await run_test('slugs-layout-not-all-pages-have-load');
43+
await run_test('param-type-inference');
4344
try {
4445
execSync('pnpm testtypes', { cwd });
4546
} catch (e) {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
/* eslint-disable */
2+
3+
/** @type {import('../../.svelte-kit/types/src/core/sync/write_types/test/param-type-inference/optional/[[optionalNarrowedParam=narrowed]]/$types').PageLoad} */
4+
export function load({ params }) {
5+
if (params.optionalNarrowedParam) {
6+
/** @type {"a" | "b"} */
7+
let a;
8+
a = params.optionalNarrowedParam;
9+
return { a };
10+
}
11+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
/**
2+
* @param {string} param
3+
* @returns {param is "a" | "b"}
4+
*/
5+
export const match = (param) => ['a', 'b'].includes(param);
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
/* eslint-disable */
2+
3+
/**
4+
* @param {string} param
5+
* @returns {boolean}
6+
*/
7+
export const match = (param) => true;
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
/* eslint-disable */
2+
3+
/** @type {import('../.svelte-kit/types/src/core/sync/write_types/test/param-type-inference/required/$types').LayoutLoad} */
4+
export function load({ params }) {
5+
if (params.narrowedParam) {
6+
/** @type {"a" | "b"} */
7+
const a = params.narrowedParam;
8+
}
9+
10+
if (params.regularParam) {
11+
/** @type {"a" | "b"} */
12+
let a;
13+
14+
//@ts-expect-error
15+
a = params.regularParam;
16+
17+
/** @type {string} b*/
18+
const b = params.regularParam;
19+
}
20+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
/* eslint-disable */
2+
3+
/** @type {import('../../.svelte-kit/types/src/core/sync/write_types/test/param-type-inference/required/[narrowedParam=narrowed]/$types').PageLoad} */
4+
export function load({ params }) {
5+
/** @type {"a" | "b"} */
6+
let a;
7+
a = params.narrowedParam;
8+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
/* eslint-disable */
2+
3+
/** @type {import('../../.svelte-kit/types/src/core/sync/write_types/test/param-type-inference/required/[regularParam=not_narrowed]/$types').PageLoad} */
4+
export function load({ params }) {
5+
/** @type {string} a*/
6+
const a = params.regularParam;
7+
8+
/** @type {"a" | "b"} b*/
9+
let b;
10+
11+
//@ts-expect-error
12+
b = params.regularParam;
13+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
/* eslint-disable */
2+
3+
/** @type {import('../../.svelte-kit/types/src/core/sync/write_types/test/param-type-inference/spread/[...spread=narrowed]/$types').PageLoad} */
4+
export function load({ params }) {
5+
/** @type {"a" | "b"} */
6+
let a;
7+
a = params.spread;
8+
}

0 commit comments

Comments
 (0)