fix(isolated-declarations): preserve undefined for defaulted any (#25292)

## Summary

- emit `any | undefined` for default-initialized `any` parameters followed by required parameters
- add a regression fixture for #25290 and update the existing parameter snapshot
- preserve the existing handling of `unknown`

Closes #25290
This commit is contained in:
camc314 committed 2026-08-17 09:43:42 +00:00
1 parent 9ed10b728d
commit 53f9295ac5
8 files changed
+195 -38

No files matched your search

+39 -6
View File
@@ -316,6 +316,31 @@ impl<'a> IsolatedDeclarations<'a> {
}
}
/// Infer reusable parameter-property types once so transforming the constructor and the
/// generated property cannot report the same inference diagnostic twice.
fn infer_constructor_parameter_property_types(
&self,
params: &ArenaBox<'a, FormalParameters<'a>>,
) -> Option<ArenaBox<'a, FormalParameters<'a>>> {
let mut inferred_params = None;
for (index, param) in params.items.iter().enumerate() {
if param.has_modifier()
&& !param.accessibility.is_some_and(TSAccessibility::is_private)
&& param.initializer.is_some()
&& param.type_annotation.is_none()
&& let Some(ts_type) = self.infer_type_from_formal_parameter(param)
{
let inferred_params =
inferred_params.get_or_insert_with(|| params.clone_in(self.allocator()));
inferred_params.items[index].type_annotation =
Some(TSTypeAnnotation::boxed(SPAN, ts_type, self));
}
}
inferred_params
}
/// Transform constructor parameters to class properties.
///
/// For example:
@@ -327,17 +352,16 @@ impl<'a> IsolatedDeclarations<'a> {
/// `class C { public x: string; constructor(x: string) {} }`
fn transform_constructor_parameter_properties(
&self,
function: &Function<'a>,
params: &FormalParameters<'a>,
typed_params: &FormalParameters<'a>,
) -> ArenaVec<'a, ClassElement<'a>> {
ArenaVec::from_iter_in(
function
.params
params
.items
.iter()
.filter(|param| {
// To follow up `transform_formal_parameters`'s behavior
typed_params.items.len() == function.params.items.len() || param.has_modifier()
typed_params.items.len() == params.items.len() || param.has_modifier()
})
.enumerate()
.filter_map(|(index, param)| {
@@ -347,6 +371,8 @@ impl<'a> IsolatedDeclarations<'a> {
let type_annotation =
if param.accessibility.is_some_and(TSAccessibility::is_private) {
None
} else if param.initializer.is_some() {
param.type_annotation.clone_in(self.allocator())
} else {
// transformed params will definitely have type annotation
typed_params.items[index].type_annotation.clone_in(self.allocator())
@@ -552,11 +578,18 @@ impl<'a> IsolatedDeclarations<'a> {
let is_private =
method.accessibility.is_some_and(TSAccessibility::is_private);
let inferred_params =
self.infer_constructor_parameter_property_types(&function.params);
let function_params =
inferred_params.as_ref().unwrap_or(&function.params);
let params =
self.transform_formal_parameters(&function.params, is_private);
self.transform_formal_parameters(function_params, is_private);
elements.splice(
0..0,
self.transform_constructor_parameter_properties(function, &params),
self.transform_constructor_parameter_properties(
function_params,
&params,
),
);
if is_function_overloads && function.body.is_some() {
@@ -86,22 +86,21 @@ impl<'a> IsolatedDeclarations<'a> {
.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 is_remaining_params_have_required || (param.optional && param.has_modifier())
if (is_remaining_params_have_required
|| (param.optional && param.has_modifier()))
&& !contains_undefined(&ts_type)
{
if matches!(ts_type, TSType::TSTypeReference(_)) {
self.error(implicitly_adding_undefined_to_type(param.span));
} else if !ts_type.is_maybe_undefined() {
// union with `undefined`
return TSTypeAnnotation::boxed(
SPAN,
TSType::new_ts_union_type(
SPAN,
[ts_type, TSType::new_ts_undefined_keyword(SPAN, self)],
self,
),
self,
);
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);
}
self.error(implicitly_adding_undefined_to_type(param.span));
}
TSTypeAnnotation::boxed(SPAN, ts_type, self)
@@ -188,3 +187,40 @@ impl<'a> IsolatedDeclarations<'a> {
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 {
match ts_type {
TSType::TSUndefinedKeyword(_) => true,
TSType::TSUnionType(union) => union.types.iter().any(contains_undefined),
TSType::TSParenthesizedType(parenthesized) => {
contains_undefined(&parenthesized.type_annotation)
}
_ => false,
}
}
/// Whether TypeScript can syntactically add `undefined` without using type information.
fn can_add_undefined(ts_type: &TSType<'_>) -> bool {
if ts_type.is_keyword() {
return true;
}
match ts_type {
TSType::TSLiteralType(_)
| TSType::TSFunctionType(_)
| TSType::TSConstructorType(_)
| TSType::TSArrayType(_)
| TSType::TSTupleType(_)
| TSType::TSTypeLiteral(_)
| TSType::TSTemplateLiteralType(_)
| TSType::TSThisType(_) => true,
TSType::TSParenthesizedType(parenthesized) => {
can_add_undefined(&parenthesized.type_annotation)
}
TSType::TSUnionType(union) => union.types.iter().all(can_add_undefined),
TSType::TSIntersectionType(intersection) => {
intersection.types.iter().all(can_add_undefined)
}
_ => false,
}
}
@@ -67,6 +67,14 @@ export class PrivateFieldsWithConstructorAssignments {
constructor(public first: number) {}
}
export class DefaultedAnyParameterProperty {
constructor(public value: any = 1, required: string) {}
}
export class DefaultedArrayParameterProperty {
constructor(public values = [], required: string) {}
}
export class PrivateMethodClass {
private good(a): void {}
@@ -29,7 +29,16 @@ export const fooBad2 = ({a, b} = { a: 1, b: 2 }): number => {
export function withAny(a: any = 1, b: string): void { }
export function withAnyUnion(a: any | string = 1, b: string): void { }
export function withAnyArrayUnion(a: any | number[] = [], b: string): void { }
export function withAnyUndefinedUnion(a: any | undefined = 1, b: string): void { }
type Foo = {};
export function withAnyTypeReferenceUnion(a: any | Foo = {}, b: string): void { }
export function withAnyTypeReferenceUndefinedUnion(a: any | Foo | undefined = {}, b: string): void { }
export function withAnyTypeOperatorUnion(a: any | keyof {} = 1, b: string): void { }
export function withAnyIntersectionTypeReferenceUnion(a: any | (Foo & {}) = {}, b: string): void { }
export function withUnknown(a: unknown = 1, b: string): void { }
export function withUnknownUnion(a: unknown | string = 1, b: string): void { }
export function withTypeAssertion(a = /regular-repression-cannot-infer/ as any): void { }
@@ -0,0 +1,6 @@
export function ssrGetDynamicModelProps(
existingProps: any = {},
model: unknown,
): { checked: true } | { value: any } | null {
return {};
}
@@ -63,6 +63,14 @@ export declare class PrivateFieldsWithConstructorAssignments {
private second;
constructor(first: number);
}
export declare class DefaultedAnyParameterProperty {
value: any;
constructor(value: any | undefined, required: string);
}
export declare class DefaultedArrayParameterProperty {
values: unknown;
constructor(values: unknown | undefined, required: string);
}
export declare class PrivateMethodClass {
private good;
private get goodGetter();
@@ -145,40 +153,48 @@ export declare class ProtectedConstructorWithPrivateParamPropertyInferredDefault
==================== Errors ====================
x TS9017: Only const arrays can be inferred with --isolatedDeclarations.
,-[75:31]
74 | export class DefaultedArrayParameterProperty {
75 | constructor(public values = [], required: string) {}
: ^^
76 | }
`----
x TS9038: Computed property names on class or object literals cannot be
| inferred with --isolatedDeclarations.
,-[81:14]
80 | public get badGetter() {
81 | return {[('x')]: 1};
,-[89:14]
88 | public get badGetter() {
89 | return {[('x')]: 1};
: ^^^^^
82 | }
90 | }
`----
x TS9011: Parameter must have an explicit type annotation with
| --isolatedDeclarations.
,-[79:14]
78 | export class PublicMethodClass {
79 | public bad(a): void {}
,-[87:14]
86 | export class PublicMethodClass {
87 | public bad(a): void {}
: ^
80 | public get badGetter() {
88 | public get badGetter() {
`----
x TS9011: Parameter must have an explicit type annotation with
| --isolatedDeclarations.
,-[192:3]
191 | private constructor(
192 | public readonly a = new InferredDefault(),
,-[200:3]
199 | private constructor(
200 | public readonly a = new InferredDefault(),
: ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
193 | ) {}
201 | ) {}
`----
x TS9011: Parameter must have an explicit type annotation with
| --isolatedDeclarations.
,-[201:3]
200 | protected constructor(
201 | private readonly a = new InferredDefault(),
,-[209:3]
208 | protected constructor(
209 | private readonly a = new InferredDefault(),
: ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
202 | ) {}
210 | ) {}
`----
@@ -15,14 +15,24 @@ export declare function fnDeclBad2<T>(p: T, r2: T): void;
export declare function fnDeclBad3<T>(p: T, r2: T, rParam?: T): void;
export declare function fooBad(): number;
export declare const fooBad2: () => number;
export declare function withAny(a: any, b: string): void;
export declare function withUnknown(a: unknown, b: string): void;
export declare function withAny(a: any | undefined, b: string): void;
export declare function withAnyUnion(a: any | string | undefined, b: string): void;
export declare function withAnyArrayUnion(a: any | number[] | undefined, b: string): void;
export declare function withAnyUndefinedUnion(a: any | undefined, b: string): void;
type Foo = {};
export declare function withAnyTypeReferenceUnion(a: any | Foo, b: string): void;
export declare function withAnyTypeReferenceUndefinedUnion(a: any | Foo | undefined, b: string): void;
export declare function withAnyTypeOperatorUnion(a: any | keyof {}, b: string): void;
export declare function withAnyIntersectionTypeReferenceUnion(a: any | (Foo & {}), b: string): void;
export declare function withUnknown(a: unknown | undefined, b: string): void;
export declare function withUnknownUnion(a: unknown | string | undefined, b: string): void;
export declare function withTypeAssertion(a?: any): void;
export declare function restWithDefault(...[value, options]: [string, object]): void;
export declare function restWithNestedDefault(...[a, { b, c }]: [number, {
b: number;
c: number;
}]): void;
export {};
==================== Errors ====================
@@ -81,5 +91,32 @@ export declare function restWithNestedDefault(...[a, { b, c }]: [number, {
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]
37 | export function withAnyTypeReferenceUndefinedUnion(a: any | Foo | undefined = {}, b: string): void { }
38 | export function withAnyTypeOperatorUnion(a: any | keyof {} = 1, b: string): void { }
: ^^^^^^^^^^^^^^^^^^^^^
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 { }
`----
```
@@ -0,0 +1,12 @@
---
source: crates/oxc_isolated_declarations/tests/mod.rs
input_file: crates/oxc_isolated_declarations/tests/fixtures/issue-25290.ts
---
```
==================== .D.TS ====================
export declare function ssrGetDynamicModelProps(existingProps: any | undefined, model: unknown): {
checked: true;
} | {
value: any;
} | null;