fix(isolated-declarations): preserve unresolved parameter types (#25909)

#25292 made isolated declaration emit report a false TS9025 for a defaulted parameter whose annotation is an indexed access type. This breaks Vite's `UserConfig["base"]` case when Rolldown upgrades Oxc, even though the indexed property already includes `undefined`.

Oxc's isolated declaration emitter has no type checker, so it cannot safely resolve indexed accesses, type references, conditional types, or similar annotations. This change uses a syntax-only classification: widen or diagnose types whose syntax is conclusive, and preserve checker-dependent types rather than emit an unstable error. The union/intersection rules retain the distinct behavior of `any`, `never`, `unknown`, and `void`, and an explicit `undefined` union member is never duplicated.

Regression coverage includes the Rolldown/Vite case and a compact matrix of the relevant type algebra. The implementation and tests were reduced to one commit after review.

Validated with the isolated-declarations tests, strict Clippy, the all-features workspace suite, lint, docs, AST generation, and TypeScript 5.6.1-rc/6.0.3 type-algebra checks. The full local `just ready` run stopped only at a runner-dependent source-map snapshot because the repository expects Node 26.5.0 and this machine has 25.8.1; the remaining suite passed with that and the known terminal-color snapshot group skipped.

Follow-up to #25292. Related Rolldown failure: https://github.com/rolldown/rolldown/actions/runs/32158377691/job/95782148042?pr=10707

Disclosure: this change was prepared and verified with AI assistance (Codex).
This commit is contained in:
Dunqing committed 2026-08-19 07:36:24 +00:00
1 parent 83cb5163c0
commit 134fb2228d
6 files changed
+334 -70

No files matched your search

+137 -16
View File
@@ -84,23 +84,36 @@ impl<'a> IsolatedDeclarations<'a> {
new_type
})
.map(|ts_type| {
// jf next param is not optional and current param is assignment pattern
// we need to add undefined to it's type
// If a defaulted parameter is followed by a required parameter, declaration
// emit may need to add `undefined` to its type because the parameter cannot be
// marked optional. Preserve annotations whose resolved type is unknown: unlike
// TypeScript, isolated declaration emit does not have a type checker available
// to determine whether they already include `undefined`.
if (is_remaining_params_have_required
|| (param.optional && param.has_modifier()))
&& !contains_undefined(&ts_type)
&& !has_explicit_undefined_union_member(&ts_type)
{
if can_add_undefined(&ts_type) {
let undefined = TSType::new_ts_undefined_keyword(SPAN, self);
let ts_type = if let TSType::TSUnionType(mut union) = ts_type {
union.types.push(undefined);
TSType::TSUnionType(union)
} else {
TSType::new_ts_union_type(SPAN, [ts_type, undefined], self)
};
return TSTypeAnnotation::boxed(SPAN, ts_type, self);
let presence = undefined_presence(&ts_type);
let can_add = can_add_undefined(&ts_type);
match (presence, can_add) {
(UndefinedPresence::Present, _)
| (UndefinedPresence::Unresolved, false) => {}
(_, true) => {
// Adding `undefined` is either required or redundant after
// TypeScript resolves the type.
let undefined = TSType::new_ts_undefined_keyword(SPAN, self);
let ts_type = if let TSType::TSUnionType(mut union) = ts_type {
union.types.push(undefined);
TSType::TSUnionType(union)
} else {
TSType::new_ts_union_type(SPAN, [ts_type, undefined], self)
};
return TSTypeAnnotation::boxed(SPAN, ts_type, self);
}
(_, false) => {
self.error(implicitly_adding_undefined_to_type(param.span));
}
}
self.error(implicitly_adding_undefined_to_type(param.span));
}
TSTypeAnnotation::boxed(SPAN, ts_type, self)
@@ -188,13 +201,121 @@ pub fn get_function_span(func: &Function<'_>) -> Span {
func.id.as_ref().map_or_else(|| Span::empty(func.params.span.start), |id| id.span)
}
fn contains_undefined(ts_type: &TSType<'_>) -> bool {
/// Syntax-only categories that remain distinct while folding unions and intersections.
#[derive(Clone, Copy)]
enum UndefinedPresence {
Present,
Absent,
/// `any` accepts `undefined` on its own but makes an intersection checker-dependent.
Any,
/// `never` excludes `undefined` and absorbs every other intersection operand.
Never,
/// `unknown` behaves like `Absent` for declaration emit but is the identity element of an
/// intersection.
UnknownKeyword,
/// `void` excludes `undefined` on its own but retains it when intersected with `undefined`.
Void,
/// Resolving this annotation requires checker information.
Unresolved,
}
impl UndefinedPresence {
/// Combine the classifications of two union members.
fn union(self, other: Self) -> Self {
use UndefinedPresence::{Absent, Any, Never, Present, UnknownKeyword, Unresolved, Void};
match (self, other) {
(Unresolved, _) | (_, Unresolved) => Unresolved,
(Any, _) | (_, Any) => Any,
(UnknownKeyword, _) | (_, UnknownKeyword) => UnknownKeyword,
(Present, _) | (_, Present) => Present,
(Void, _) | (_, Void) => Void,
(Absent, _) | (_, Absent) => Absent,
(Never, Never) => Never,
}
}
/// Combine the classifications of two intersection members.
fn intersection(self, other: Self) -> Self {
use UndefinedPresence::{Absent, Any, Never, Present, UnknownKeyword, Unresolved, Void};
match (self, other) {
(Never, _) | (_, Never) => Never,
(Unresolved | Any, _) | (_, Unresolved | Any) => Unresolved,
(Absent, _) | (_, Absent) => Absent,
(Present, _) | (_, Present) => Present,
(Void, _) | (_, Void) => Void,
(UnknownKeyword, UnknownKeyword) => UnknownKeyword,
}
}
}
/// Classify whether annotation syntax proves that the parameter type includes `undefined`.
fn undefined_presence(ts_type: &TSType<'_>) -> UndefinedPresence {
match ts_type {
TSType::TSUndefinedKeyword(_) => UndefinedPresence::Present,
TSType::TSAnyKeyword(_) => UndefinedPresence::Any,
TSType::TSUnknownKeyword(_) => UndefinedPresence::UnknownKeyword,
TSType::TSBigIntKeyword(_)
| TSType::TSBooleanKeyword(_)
| TSType::TSIntrinsicKeyword(_)
| TSType::TSNullKeyword(_)
| TSType::TSNumberKeyword(_)
| TSType::TSObjectKeyword(_)
| TSType::TSStringKeyword(_)
| TSType::TSSymbolKeyword(_)
| TSType::TSLiteralType(_)
| TSType::TSFunctionType(_)
| TSType::TSConstructorType(_)
| TSType::TSArrayType(_)
| TSType::TSTupleType(_)
| TSType::TSMappedType(_)
| TSType::TSTypeLiteral(_)
| TSType::TSTypeOperatorType(_)
| TSType::TSTemplateLiteralType(_)
| TSType::TSThisType(_) => UndefinedPresence::Absent,
TSType::TSNeverKeyword(_) => UndefinedPresence::Never,
TSType::TSVoidKeyword(_) => UndefinedPresence::Void,
TSType::TSParenthesizedType(parenthesized) => {
undefined_presence(&parenthesized.type_annotation)
}
TSType::TSUnionType(union) => union
.types
.iter()
.map(undefined_presence)
.fold(UndefinedPresence::Never, UndefinedPresence::union),
// Resolving these types requires type information. Treat them as unknown so declaration
// emit does not report a false-positive TS9025 for a type that already contains
// `undefined`. Some variants cannot occur directly as a parameter annotation, but listing
// them keeps this classification exhaustive as new `TSType` variants are added.
TSType::TSConditionalType(_)
| TSType::TSImportType(_)
| TSType::TSIndexedAccessType(_)
| TSType::TSInferType(_)
| TSType::TSNamedTupleMember(_)
| TSType::TSTypePredicate(_)
| TSType::TSTypeQuery(_)
| TSType::TSTypeReference(_)
| TSType::JSDocNullableType(_)
| TSType::JSDocNonNullableType(_)
| TSType::JSDocUnknownType(_) => UndefinedPresence::Unresolved,
TSType::TSIntersectionType(intersection) => intersection
.types
.iter()
.map(undefined_presence)
.fold(UndefinedPresence::UnknownKeyword, UndefinedPresence::intersection),
}
}
/// Whether the annotation itself already has an explicit `undefined` union member.
///
/// Do not descend through intersections: `undefined & string` excludes `undefined`, whereas a
/// parenthesized or nested union member still spells it explicitly.
fn has_explicit_undefined_union_member(ts_type: &TSType<'_>) -> bool {
match ts_type {
TSType::TSUndefinedKeyword(_) => true,
TSType::TSUnionType(union) => union.types.iter().any(contains_undefined),
TSType::TSParenthesizedType(parenthesized) => {
contains_undefined(&parenthesized.type_annotation)
has_explicit_undefined_union_member(&parenthesized.type_annotation)
}
TSType::TSUnionType(union) => union.types.iter().any(has_explicit_undefined_union_member),
_ => false,
}
}
@@ -0,0 +1,21 @@
export interface UserConfig {
base?: string | false;
}
declare const configDefaults: Required<UserConfig>;
export type Base = UserConfig["base"];
export function resolveBaseUrl(
base: UserConfig["base"] = configDefaults.base,
isBuild: boolean,
): string {
return "";
}
export function resolveAliasedBaseUrl(
base: Base = configDefaults.base,
isBuild: boolean,
): string {
return "";
}
@@ -0,0 +1,68 @@
interface Source {
value: string;
}
export function mapped(
value: { [K in keyof Source]: Source[K] } = { value: "" },
required: string,
): void {}
export function keyOf(
value: keyof Source = "value",
required: string,
): void {}
export function readonlyTuple(
value: readonly [string] = [""],
required: string,
): void {}
export function intersection(
value: { optional?: string } & number[] = [],
required: string,
): void {}
export function operatorIntersection(
value: (keyof Source) & string = "value",
required: string,
): void {}
export function unknownOperatorIntersection(
value: unknown & keyof Source = "value",
required: string,
): void {}
export function unresolvedIntersection<T>(
value: T & string = "" as T & string,
required: string,
): void {}
export function allUnknownIntersection(
value: unknown & unknown = undefined,
required: string,
): void {}
export function unresolvedUndefinedUnionIntersection<T>(
value: (T | undefined) & string = "" as (T | undefined) & string,
required: string,
): void {}
export function unknownUndefinedUnionOperatorIntersection(
value: (unknown | undefined) & keyof Source = "value",
required: string,
): void {}
export function anyUndefinedUnionOperatorIntersection(
value: (any | undefined) & keyof Source = "value",
required: string,
): void {}
export function unresolvedNeverOperatorIntersection<T>(
value: T & never & keyof Source = undefined as never,
required: string,
): void {}
export function voidUnionUndefinedIntersection(
value: (void | string) & undefined = undefined,
required: string,
): void {}
@@ -0,0 +1,13 @@
---
source: crates/oxc_isolated_declarations/tests/mod.rs
input_file: crates/oxc_isolated_declarations/tests/fixtures/default-parameter-indexed-access.ts
---
```
==================== .D.TS ====================
export interface UserConfig {
base?: string | false;
}
export type Base = UserConfig["base"];
export declare function resolveBaseUrl(base: UserConfig["base"], isBuild: boolean): string;
export declare function resolveAliasedBaseUrl(base: Base, isBuild: boolean): string;
@@ -0,0 +1,95 @@
---
source: crates/oxc_isolated_declarations/tests/mod.rs
input_file: crates/oxc_isolated_declarations/tests/fixtures/default-parameter-type-kinds.ts
---
```
==================== .D.TS ====================
interface Source {
value: string;
}
export declare function mapped(value: { [K in keyof Source]: Source[K] }, required: string): void;
export declare function keyOf(value: keyof Source, required: string): void;
export declare function readonlyTuple(value: readonly [string], required: string): void;
export declare function intersection(value: {
optional?: string;
} & number[] | undefined, required: string): void;
export declare function operatorIntersection(value: (keyof Source) & string, required: string): void;
export declare function unknownOperatorIntersection(value: unknown & keyof Source, required: string): void;
export declare function unresolvedIntersection<T>(value: T & string, required: string): void;
export declare function allUnknownIntersection(value: unknown & unknown | undefined, required: string): void;
export declare function unresolvedUndefinedUnionIntersection<T>(value: (T | undefined) & string, required: string): void;
export declare function unknownUndefinedUnionOperatorIntersection(value: (unknown | undefined) & keyof Source, required: string): void;
export declare function anyUndefinedUnionOperatorIntersection(value: (any | undefined) & keyof Source, required: string): void;
export declare function unresolvedNeverOperatorIntersection<T>(value: T & never & keyof Source, required: string): void;
export declare function voidUnionUndefinedIntersection(value: (void | string) & undefined, required: string): void;
export {};
==================== Errors ====================
x TS9025: Declaration emit for this parameter requires implicitly adding
| undefined to it's type. This is not supported with --isolatedDeclarations.
,-[6:3]
5 | export function mapped(
6 | value: { [K in keyof Source]: Source[K] } = { value: "" },
: ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
7 | required: string,
`----
x TS9025: Declaration emit for this parameter requires implicitly adding
| undefined to it's type. This is not supported with --isolatedDeclarations.
,-[11:3]
10 | export function keyOf(
11 | value: keyof Source = "value",
: ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
12 | required: string,
`----
x TS9025: Declaration emit for this parameter requires implicitly adding
| undefined to it's type. This is not supported with --isolatedDeclarations.
,-[16:3]
15 | export function readonlyTuple(
16 | value: readonly [string] = [""],
: ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
17 | required: string,
`----
x TS9025: Declaration emit for this parameter requires implicitly adding
| undefined to it's type. This is not supported with --isolatedDeclarations.
,-[26:3]
25 | export function operatorIntersection(
26 | value: (keyof Source) & string = "value",
: ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
27 | required: string,
`----
x TS9025: Declaration emit for this parameter requires implicitly adding
| undefined to it's type. This is not supported with --isolatedDeclarations.
,-[31:3]
30 | export function unknownOperatorIntersection(
31 | value: unknown & keyof Source = "value",
: ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
32 | required: string,
`----
x TS9025: Declaration emit for this parameter requires implicitly adding
| undefined to it's type. This is not supported with --isolatedDeclarations.
,-[51:3]
50 | export function unknownUndefinedUnionOperatorIntersection(
51 | value: (unknown | undefined) & keyof Source = "value",
: ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
52 | required: string,
`----
x TS9025: Declaration emit for this parameter requires implicitly adding
| undefined to it's type. This is not supported with --isolatedDeclarations.
,-[61:3]
60 | export function unresolvedNeverOperatorIntersection<T>(
61 | value: T & never & keyof Source = undefined as never,
: ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
62 | required: string,
`----
```
@@ -37,42 +37,6 @@ export {};
==================== Errors ====================
x TS9025: Declaration emit for this parameter requires implicitly adding
| undefined to it's type. This is not supported with --isolatedDeclarations.
,-[18:30]
17 | // Incorrect
18 | export function fnDeclBad<T>(p: T = [], rParam: T = "", r2: T): void { }
: ^^^^^^^^^
19 | export function fnDeclBad2<T>(p: T = [], r2: T): void { }
`----
x TS9025: Declaration emit for this parameter requires implicitly adding
| undefined to it's type. This is not supported with --isolatedDeclarations.
,-[18:41]
17 | // Incorrect
18 | export function fnDeclBad<T>(p: T = [], rParam: T = "", r2: T): void { }
: ^^^^^^^^^^^^^^
19 | export function fnDeclBad2<T>(p: T = [], r2: T): void { }
`----
x TS9025: Declaration emit for this parameter requires implicitly adding
| undefined to it's type. This is not supported with --isolatedDeclarations.
,-[19:31]
18 | export function fnDeclBad<T>(p: T = [], rParam: T = "", r2: T): void { }
19 | export function fnDeclBad2<T>(p: T = [], r2: T): void { }
: ^^^^^^^^^
20 | export function fnDeclBad3<T>(p: T = [], r2: T, rParam?: T): void { }
`----
x TS9025: Declaration emit for this parameter requires implicitly adding
| undefined to it's type. This is not supported with --isolatedDeclarations.
,-[20:31]
19 | export function fnDeclBad2<T>(p: T = [], r2: T): void { }
20 | export function fnDeclBad3<T>(p: T = [], r2: T, rParam?: T): void { }
: ^^^^^^^^^
21 |
`----
x TS9011: Parameter must have an explicit type annotation with
| --isolatedDeclarations.
,-[22:24]
@@ -91,15 +55,6 @@ export {};
27 | return 2;
`----
x TS9025: Declaration emit for this parameter requires implicitly adding
| undefined to it's type. This is not supported with --isolatedDeclarations.
,-[36:43]
35 | type Foo = {};
36 | export function withAnyTypeReferenceUnion(a: any | Foo = {}, b: string): void { }
: ^^^^^^^^^^^^^^^^^
37 | export function withAnyTypeReferenceUndefinedUnion(a: any | Foo | undefined = {}, b: string): void { }
`----
x TS9025: Declaration emit for this parameter requires implicitly adding
| undefined to it's type. This is not supported with --isolatedDeclarations.
,-[38:42]
@@ -109,14 +64,5 @@ export {};
39 | export function withAnyIntersectionTypeReferenceUnion(a: any | (Foo & {}) = {}, b: string): void { }
`----
x TS9025: Declaration emit for this parameter requires implicitly adding
| undefined to it's type. This is not supported with --isolatedDeclarations.
,-[39:55]
38 | export function withAnyTypeOperatorUnion(a: any | keyof {} = 1, b: string): void { }
39 | export function withAnyIntersectionTypeReferenceUnion(a: any | (Foo & {}) = {}, b: string): void { }
: ^^^^^^^^^^^^^^^^^^^^^^^^
40 | export function withUnknown(a: unknown = 1, b: string): void { }
`----
```