Skip to content

Commit 1133bcf

Browse files
types(router): enable exactOptionalPropertyTypes
Enable exactOptionalPropertyTypes: true in packages/router/tsconfig.json. - Add | undefined only where code genuinely assigns undefined - Zero runtime changes in scrollBehavior, unplugin, experimental/router - 2 unavoidable runtime changes in RouterLink and matcher (driven by ParamValueZeroOrMore) - Fix TrackedRoute.params bug (LocationQuery -> RouteParamsGeneric) - Add 10 type-level regression tests Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 5f7bbf1 commit 1133bcf

File tree

22 files changed

+169
-65
lines changed

22 files changed

+169
-65
lines changed

packages/router/__tests__/guards/extractComponentsGuards.spec.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,6 @@ describe('extractComponentsGuards', () => {
8888
})
8989

9090
it('throws if component is null', async () => {
91-
// @ts-expect-error
9291
await expect(checkGuards([InvalidRoute], 0))
9392
expect('either missing a "component(s)" or "children"').toHaveBeenWarned()
9493
})

packages/router/__tests__/guards/onBeforeRouteUpdate.spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ const component = {
2727
function withSpy(name?: string, isAsync = false) {
2828
const spy = vi.fn()
2929
const Component = defineComponent({
30-
name,
30+
...(name != null ? { name } : {}),
3131
template: `<p>${name || 'No Name'}</p>`,
3232
setup() {
3333
onBeforeRouteUpdate(spy)

packages/router/__tests__/matcher/resolve.spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ describe('RouterMatcher.resolve', () => {
3737
}
3838

3939
// add location if provided as it should be the same value
40-
if ('path' in location && !('path' in resolved)) {
40+
if ('path' in location && !('path' in resolved) && location.path != null) {
4141
resolved.path = location.path
4242
}
4343

packages/router/__tests__/utils.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ export interface RouteRecordViewLoose extends Pick<
5757
enterCallbacks: Record<string, Function[]>
5858
props: Record<string, _RouteRecordProps>
5959
aliasOf: RouteRecordNormalized | RouteRecordViewLoose | undefined
60-
children?: RouteRecordRaw[]
60+
children?: RouteRecordRaw[] | undefined
6161
components: Record<string, RouteComponent> | null | undefined
6262
}
6363

@@ -67,17 +67,17 @@ export interface RouteLocationNormalizedLoose extends RouteLocationNormalized {
6767
path: string
6868
// record?
6969
params: any
70-
redirectedFrom?: Partial<MatcherLocation>
70+
redirectedFrom?: Partial<MatcherLocation> | undefined
7171
meta: any
7272
matched: Partial<RouteRecordViewLoose>[]
7373
}
7474

7575
export interface MatcherLocationNormalizedLoose {
76-
name: string
76+
name: string | undefined
7777
path: string
7878
// record?
7979
params: any
80-
redirectedFrom?: Partial<MatcherLocation>
80+
redirectedFrom?: Partial<MatcherLocation> | undefined
8181
meta: any
8282
matched: Partial<RouteRecordViewLoose>[]
8383
instances: Record<string, any>

packages/router/src/RouterLink.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -438,7 +438,7 @@ function includesParams(
438438
const outerValue = outer[key]
439439
if (typeof innerValue === 'string') {
440440
if (innerValue !== outerValue) return false
441-
} else {
441+
} else if (isArray(innerValue)) {
442442
if (
443443
!isArray(outerValue) ||
444444
outerValue.length !== innerValue.length ||
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import { describe, it, expectTypeOf } from 'vitest'
2+
import type {
3+
ScrollPositionCoordinates,
4+
_ScrollPositionNormalized,
5+
} from './scrollBehavior'
6+
import type { RouteParamsGeneric, _RouteRecordBase } from './types'
7+
import type { ResolvedOptions } from './unplugin/options'
8+
9+
describe('exactOptionalPropertyTypes', () => {
10+
it('ScrollPositionCoordinates rejects explicit undefined for behavior', () => {
11+
// @ts-expect-error: behavior cannot be explicitly undefined under exactOptionalPropertyTypes
12+
const pos: ScrollPositionCoordinates = { behavior: undefined }
13+
void pos
14+
})
15+
16+
it('ScrollPositionCoordinates rejects explicit undefined for left', () => {
17+
// @ts-expect-error: left cannot be explicitly undefined under exactOptionalPropertyTypes
18+
const pos: ScrollPositionCoordinates = { left: undefined }
19+
void pos
20+
})
21+
22+
it('ScrollPositionCoordinates accepts valid values', () => {
23+
expectTypeOf<ScrollPositionCoordinates>().toEqualTypeOf<{
24+
behavior?: ScrollBehavior
25+
left?: number
26+
top?: number
27+
}>()
28+
})
29+
30+
it('_ScrollPositionNormalized rejects explicit undefined for behavior', () => {
31+
// @ts-expect-error: behavior cannot be explicitly undefined under exactOptionalPropertyTypes
32+
const pos: _ScrollPositionNormalized = {
33+
behavior: undefined,
34+
left: 0,
35+
top: 0,
36+
}
37+
void pos
38+
})
39+
40+
it('_RouteRecordBase meta does not allow undefined', () => {
41+
expectTypeOf<_RouteRecordBase['meta']>().not.toEqualTypeOf<undefined>()
42+
})
43+
44+
it('RouteParamsGeneric accepts undefined values for optional repeatable params', () => {
45+
// RouteParamsGeneric includes `| undefined` because
46+
// ParamValueZeroOrMore<false> = string[] | undefined
47+
// for optional repeatable params like [[files]]+
48+
const params: RouteParamsGeneric = { id: 'abc', files: undefined }
49+
expectTypeOf(params).toEqualTypeOf<RouteParamsGeneric>()
50+
})
51+
52+
it('ResolvedOptions has non-nullable extensions after resolveOptions', () => {
53+
expectTypeOf<ResolvedOptions['extensions']>().toEqualTypeOf<string[]>()
54+
})
55+
56+
it('ResolvedOptions has non-nullable root after resolveOptions', () => {
57+
expectTypeOf<ResolvedOptions['root']>().toEqualTypeOf<string>()
58+
})
59+
60+
it('ResolvedOptions has non-nullable getRouteName after resolveOptions', () => {
61+
expectTypeOf<
62+
ResolvedOptions['getRouteName']
63+
>().not.toEqualTypeOf<undefined>()
64+
})
65+
66+
it('ResolvedOptions has non-nullable dts after resolveOptions', () => {
67+
expectTypeOf<ResolvedOptions['dts']>().not.toEqualTypeOf<undefined>()
68+
})
69+
})

packages/router/src/experimental/data-loaders/auto-exports.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ export interface AutoExportLoadersOptions {
5454
* Root of the project. All paths are resolved relatively to this one.
5555
* @default `process.cwd()`
5656
*/
57-
root?: string
57+
root?: string | undefined
5858
}
5959

6060
/**
@@ -76,6 +76,8 @@ export function AutoExportLoaders({
7676
transform: {
7777
order: 'post',
7878
filter: {
79+
// @ts-expect-error: unplugin's StringFilter adds `| undefined` to optional include/exclude
80+
// but vite's StringFilter omits it; structurally compatible at runtime
7981
id: transformFilter,
8082
},
8183

packages/router/src/experimental/data-loaders/defineColadaLoader.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ import type {
4444
import { useRoute, useRouter } from '../../useApi'
4545
import type { Router } from '../../router'
4646
import type { LocationQuery } from '../../query'
47+
import type { RouteParamsGeneric } from '../../types'
4748

4849
/**
4950
* Creates a Pinia Colada data loader with `data` is always defined.
@@ -729,7 +730,7 @@ export interface DataLoaderColadaEntry<
729730

730731
interface TrackedRoute {
731732
ready: boolean
732-
params: Partial<LocationQuery>
733+
params: Partial<RouteParamsGeneric>
733734
query: Partial<LocationQuery>
734735
hash: { v: string | null }
735736
}

packages/router/src/experimental/data-loaders/utils.ts

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import { LocationQuery } from '../../query'
21
import { Router } from '../../router'
32
import { RouteLocationNormalizedLoaded } from '../../typed-routes'
43
import type { DataLoaderEntryBase, UseDataLoader } from './createDataLoader'
@@ -113,10 +112,10 @@ function trackObjectReads<T extends Record<string, unknown>>(obj: T) {
113112
* @param outer - the bigger params
114113
* @param inner - the smaller params
115114
*/
116-
export function isSubsetOf(
117-
inner: Partial<LocationQuery>,
118-
outer: LocationQuery
119-
): boolean {
115+
export function isSubsetOf<
116+
V extends string | null | undefined,
117+
T extends Record<string, V | V[]>,
118+
>(inner: Partial<T>, outer: T): boolean {
120119
for (const key in inner) {
121120
const innerValue = inner[key]
122121
const outerValue = outer[key]

packages/router/src/experimental/route-resolver/matchers/matcher-pattern.ts

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -230,11 +230,13 @@ export class MatcherPatternPathDynamic<
230230
): string {
231231
let paramIndex = 0
232232
let paramName: keyof TParamsOptions
233-
let parser: (TParamsOptions &
234-
Record<
235-
string,
236-
MatcherPatternPathDynamic_ParamOptions<any, any>
237-
>)[keyof TParamsOptions][0]
233+
let parser:
234+
| (TParamsOptions &
235+
Record<
236+
string,
237+
MatcherPatternPathDynamic_ParamOptions<any, any>
238+
>)[keyof TParamsOptions][0]
239+
| undefined
238240
let repeatable: boolean | undefined
239241
let optional: boolean | undefined
240242
let value: ReturnType<NonNullable<ParamParser['set']>> | undefined

0 commit comments

Comments
 (0)