diff --git a/.changeset/great-bags-heal.md b/.changeset/great-bags-heal.md new file mode 100644 index 000000000000..eaf1dc6d55b4 --- /dev/null +++ b/.changeset/great-bags-heal.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': patch +--- + +feat: infer route parameter type from matcher's guard check if applicable diff --git a/packages/kit/src/core/sync/write_types/index.js b/packages/kit/src/core/sync/write_types/index.js index e5f9afb76c57..d21030e01109 100644 --- a/packages/kit/src/core/sync/write_types/index.js +++ b/packages/kit/src/core/sync/write_types/index.js @@ -188,10 +188,18 @@ function update_types(config, routes, route, to_delete = new Set()) { // add 'Expand' helper // Makes sure a type is "repackaged" and therefore more readable declarations.push('type Expand = T extends infer O ? { [K in keyof O]: O[K] } : never;'); + + // returns the predicate of a matcher's type guard - or string if there is no type guard declarations.push( - `type RouteParams = { ${route.params - .map((param) => `${param.name}${param.optional ? '?' : ''}: string`) - .join('; ')} }` + // TS complains on infer U, which seems weird, therefore ts-ignore it + [ + '// @ts-ignore', + 'type MatcherParam = M extends (param : string) => param is infer U ? U extends string ? U : string : string;' + ].join('\n') + ); + + declarations.push( + 'type RouteParams = ' + generate_params_type(route.params, outdir, config) + ';' ); if (route.params.length > 0) { @@ -265,7 +273,8 @@ function update_types(config, routes, route, to_delete = new Set()) { if (route.layout) { let all_pages_have_load = true; - const layout_params = new Set(); + /** @type {import('types').RouteParam[]} */ + const layout_params = []; const ids = ['RouteId']; route.layout.child_pages?.forEach((page) => { @@ -274,7 +283,9 @@ function update_types(config, routes, route, to_delete = new Set()) { if (leaf.route.page) ids.push(`"${leaf.route.id}"`); for (const param of leaf.route.params) { - layout_params.add(param.name); + // skip if already added + if (layout_params.some((p) => p.name === param.name)) continue; + layout_params.push({ ...param, optional: true }); } ensureProxies(page, leaf.proxies); @@ -301,9 +312,7 @@ function update_types(config, routes, route, to_delete = new Set()) { declarations.push(`type LayoutRouteId = ${ids.join(' | ')}`); declarations.push( - `type LayoutParams = RouteParams & { ${Array.from(layout_params).map( - (param) => `${param}?: string` - )} }` + 'type LayoutParams = RouteParams & ' + generate_params_type(layout_params, outdir, config) ); const { @@ -567,6 +576,28 @@ function replace_ext_with_js(file_path) { return file_path.slice(0, -ext.length) + '.js'; } +/** + * @param {import('types').RouteParam[]} params + * @param {string} outdir + * @param {import('types').ValidatedConfig} config + */ +function generate_params_type(params, outdir, config) { + /** @param {string} matcher */ + const path_to_matcher = (matcher) => + posixify(path.relative(outdir, path.join(config.kit.files.params, matcher))); + + return `{ ${params + .map( + (param) => + `${param.name}${param.optional ? '?' : ''}: ${ + param.matcher + ? `MatcherParam` + : 'string' + }` + ) + .join('; ')} }`; +} + /** * @param {string} content * @param {boolean} is_server diff --git a/packages/kit/src/core/sync/write_types/index.spec.js b/packages/kit/src/core/sync/write_types/index.spec.js index 0c13aa04e863..d50a4d12fa82 100644 --- a/packages/kit/src/core/sync/write_types/index.spec.js +++ b/packages/kit/src/core/sync/write_types/index.spec.js @@ -18,7 +18,7 @@ async function run_test(dir) { const initial = options({}, 'config'); initial.kit.files.assets = path.resolve(cwd, 'static'); - initial.kit.files.params = path.resolve(cwd, 'params'); + initial.kit.files.params = path.resolve(cwd, dir, 'params'); initial.kit.files.routes = path.resolve(cwd, dir); initial.kit.outDir = path.resolve(cwd, path.join(dir, '.svelte-kit')); @@ -40,6 +40,7 @@ test('Creates correct $types', async () => { await run_test('layout-advanced'); await run_test('slugs'); await run_test('slugs-layout-not-all-pages-have-load'); + await run_test('param-type-inference'); try { execSync('pnpm testtypes', { cwd }); } catch (e) { diff --git a/packages/kit/src/core/sync/write_types/test/param-type-inference/optional/[[optionalNarrowedParam=narrowed]]/+page.js b/packages/kit/src/core/sync/write_types/test/param-type-inference/optional/[[optionalNarrowedParam=narrowed]]/+page.js new file mode 100644 index 000000000000..024289bef97e --- /dev/null +++ b/packages/kit/src/core/sync/write_types/test/param-type-inference/optional/[[optionalNarrowedParam=narrowed]]/+page.js @@ -0,0 +1,11 @@ +/* eslint-disable */ + +/** @type {import('../../.svelte-kit/types/src/core/sync/write_types/test/param-type-inference/optional/[[optionalNarrowedParam=narrowed]]/$types').PageLoad} */ +export function load({ params }) { + if (params.optionalNarrowedParam) { + /** @type {"a" | "b"} */ + let a; + a = params.optionalNarrowedParam; + return { a }; + } +} diff --git a/packages/kit/src/core/sync/write_types/test/param-type-inference/params/narrowed.js b/packages/kit/src/core/sync/write_types/test/param-type-inference/params/narrowed.js new file mode 100644 index 000000000000..e8d48d1ca344 --- /dev/null +++ b/packages/kit/src/core/sync/write_types/test/param-type-inference/params/narrowed.js @@ -0,0 +1,5 @@ +/** + * @param {string} param + * @returns {param is "a" | "b"} + */ +export const match = (param) => ['a', 'b'].includes(param); diff --git a/packages/kit/src/core/sync/write_types/test/param-type-inference/params/not_narrowed.js b/packages/kit/src/core/sync/write_types/test/param-type-inference/params/not_narrowed.js new file mode 100644 index 000000000000..0e2f951afadd --- /dev/null +++ b/packages/kit/src/core/sync/write_types/test/param-type-inference/params/not_narrowed.js @@ -0,0 +1,7 @@ +/* eslint-disable */ + +/** + * @param {string} param + * @returns {boolean} + */ +export const match = (param) => true; diff --git a/packages/kit/src/core/sync/write_types/test/param-type-inference/required/+layout.js b/packages/kit/src/core/sync/write_types/test/param-type-inference/required/+layout.js new file mode 100644 index 000000000000..5a67f890cd8c --- /dev/null +++ b/packages/kit/src/core/sync/write_types/test/param-type-inference/required/+layout.js @@ -0,0 +1,20 @@ +/* eslint-disable */ + +/** @type {import('../.svelte-kit/types/src/core/sync/write_types/test/param-type-inference/required/$types').LayoutLoad} */ +export function load({ params }) { + if (params.narrowedParam) { + /** @type {"a" | "b"} */ + const a = params.narrowedParam; + } + + if (params.regularParam) { + /** @type {"a" | "b"} */ + let a; + + //@ts-expect-error + a = params.regularParam; + + /** @type {string} b*/ + const b = params.regularParam; + } +} diff --git a/packages/kit/src/core/sync/write_types/test/param-type-inference/required/[narrowedParam=narrowed]/+page.js b/packages/kit/src/core/sync/write_types/test/param-type-inference/required/[narrowedParam=narrowed]/+page.js new file mode 100644 index 000000000000..395ae659aa1d --- /dev/null +++ b/packages/kit/src/core/sync/write_types/test/param-type-inference/required/[narrowedParam=narrowed]/+page.js @@ -0,0 +1,8 @@ +/* eslint-disable */ + +/** @type {import('../../.svelte-kit/types/src/core/sync/write_types/test/param-type-inference/required/[narrowedParam=narrowed]/$types').PageLoad} */ +export function load({ params }) { + /** @type {"a" | "b"} */ + let a; + a = params.narrowedParam; +} diff --git a/packages/kit/src/core/sync/write_types/test/param-type-inference/required/[regularParam=not_narrowed]/+page.js b/packages/kit/src/core/sync/write_types/test/param-type-inference/required/[regularParam=not_narrowed]/+page.js new file mode 100644 index 000000000000..6fd50653a023 --- /dev/null +++ b/packages/kit/src/core/sync/write_types/test/param-type-inference/required/[regularParam=not_narrowed]/+page.js @@ -0,0 +1,13 @@ +/* eslint-disable */ + +/** @type {import('../../.svelte-kit/types/src/core/sync/write_types/test/param-type-inference/required/[regularParam=not_narrowed]/$types').PageLoad} */ +export function load({ params }) { + /** @type {string} a*/ + const a = params.regularParam; + + /** @type {"a" | "b"} b*/ + let b; + + //@ts-expect-error + b = params.regularParam; +} diff --git a/packages/kit/src/core/sync/write_types/test/param-type-inference/spread/[...spread=narrowed]/+page.js b/packages/kit/src/core/sync/write_types/test/param-type-inference/spread/[...spread=narrowed]/+page.js new file mode 100644 index 000000000000..f26d10e3aa74 --- /dev/null +++ b/packages/kit/src/core/sync/write_types/test/param-type-inference/spread/[...spread=narrowed]/+page.js @@ -0,0 +1,8 @@ +/* eslint-disable */ + +/** @type {import('../../.svelte-kit/types/src/core/sync/write_types/test/param-type-inference/spread/[...spread=narrowed]/$types').PageLoad} */ +export function load({ params }) { + /** @type {"a" | "b"} */ + let a; + a = params.spread; +}