fix(codegen): tighten conditional-type and constructor-type whitespace when minifying (#23033)

## What

Batches the remaining low-risk TS-minify whitespace patterns surfaced by the whitespace analyzer.

### Conditional types
`?`/`:` are now tight when minifying, like a JS conditional expression:

| Input | Before | After |
| --- | --- | --- |
| `A extends B ? C : D` | `A extends B ? C : D` | `A extends B?C:D` |

The leading `extends` space is emitted only when the check type ends in an identifier (so `T extends …` keeps it, `{} extends …` doesn't). The space *after* `extends` is kept to avoid merging a following type keyword (`keyof`, etc.).

### Constructor types
No space after `new` before `(`/`<`:

| Input | Before | After |
| --- | --- | --- |
| `new () => Foo` | `new () => Foo` | `new()=>Foo` |
| `abstract new (x: number) => Foo` | `abstract new (x: number) => Foo` | `abstract new(x:number)=>Foo` |

Pretty-print output is unchanged; `ts` minify snapshot updated. Verified round-trip.
This commit is contained in:
Boshen committed 2026-06-06 18:14:46 +00:00
1 parent 95dd3a2970
commit 325c94fefb
3 files changed
+38 -5

No files matched your search

+21 -4
View File
@@ -3331,13 +3331,24 @@ impl Gen for TSConditionalType<'_> {
self.check_type.print(p, ctx);
},
);
p.print_str(" extends ");
p.print_soft_space();
p.print_space_before_identifier();
p.print_str("extends ");
p.wrap(matches!(self.extends_type, TSType::TSConditionalType(_)), |p| {
self.extends_type.print(p, ctx);
});
p.print_str(" ? ");
p.print_soft_space();
// Avoid merging into `??` when the extends type ends with `?` (postfix
// JSDoc nullable, e.g. `A extends C? ? D : E`).
if p.last_byte() == Some(b'?') {
p.print_hard_space();
}
p.print_str("?");
p.print_soft_space();
self.true_type.print(p, ctx);
p.print_str(" : ");
p.print_soft_space();
p.print_str(":");
p.print_soft_space();
self.false_type.print(p, ctx);
}
}
@@ -3458,6 +3469,11 @@ impl Gen for JSDocNullableType<'_> {
self.type_annotation.print(p, ctx);
p.print_ascii_byte(b'?');
} else {
// Avoid merging into `??` after a preceding `?` (e.g. a conditional type
// `A extends B ? ?C : D` minified).
if p.last_byte() == Some(b'?') {
p.print_hard_space();
}
p.print_ascii_byte(b'?');
self.type_annotation.print(p, ctx);
}
@@ -4035,7 +4051,8 @@ impl Gen for TSConstructorType<'_> {
if self.r#abstract {
p.print_str("abstract ");
}
p.print_str("new ");
p.print_str("new");
p.print_soft_space();
if let Some(type_parameters) = &self.type_parameters {
type_parameters.print(p, ctx);
}
@@ -70,7 +70,7 @@ export{Foo,type Bar}from"foo";
########## 15
type A<T> = { [K in keyof T as K extends string ? B<K> : K ]: T[K] }
----------
type A<T>={[K in keyof T as K extends string ? B<K> : K]:T[K]};
type A<T>={[K in keyof T as K extends string?B<K>:K]:T[K]};
########## 16
class A {readonly type = 'frame'}
----------
@@ -207,6 +207,22 @@ fn minify_return_type_colon() {
min("function h(this: Foo): void {}", "function h(this:Foo):void{}");
}
#[test]
fn minify_ts_type_space() {
let min = |src: &str, expected: &str| {
test_options_with_source_type(src, expected, SourceType::ts(), CodegenOptions::minify());
};
// Conditional type: `?`/`:` are tight like a JS conditional expression.
min("type T = A extends B ? C : D;", "type T=A extends B?C:D;");
min("type T = A extends {} ? B : C;", "type T=A extends {}?B:C;");
// Constructor type: no space after `new` before `(`/`<`.
min("type N = new () => Foo;", "type N=new()=>Foo;");
min("type N = abstract new (x: number) => Foo;", "type N=abstract new(x:number)=>Foo;");
// A JSDoc-nullable branch must not merge into `??`.
min("type T = A extends B ? ?C : D;", "type T=A extends B? ?C:D;");
min("type T = A extends C? ? D : E;", "type T=A extends C? ?D:E;");
}
#[test]
fn ts_as_expression_in_binary_expr() {
test_idempotency("key in (that as object)");