fix(isolated-declarations): emit const readonly fields as types (#24288)

## Summary

Fixes #24284.

TypeScript 6.0.3 emits bare readonly literal class fields as value initializers, but emits readonly `as const` literal fields as type annotations. Oxc previously unwrapped `as const` in the readonly class-property initializer path, so `static readonly A = "a" as const` became `static readonly A = "a";` instead of `static readonly A: "a";`.

This changes the readonly class-property handling to preserve value initializers only for bare literals. Const assertions now flow through the existing const-expression type conversion.
This commit is contained in:
camc314 committed 2026-07-08 10:55:22 +00:00
1 parent 042222fbe5
commit f17514b21c
4 files changed
+41 -14

No files matched your search

+9 -10
View File
@@ -105,8 +105,10 @@ impl<'a> IsolatedDeclarations<'a> {
type_annotation = property.type_annotation.clone_in(self.allocator());
} else if let Some(expr) = property.value.as_ref() {
let ts_type = if property.readonly {
// Keep literal const initializers on readonly class properties to match TS d.ts emit.
if let Some(initializer) = self.get_literal_const_initializer(expr) {
// Keep literal initializers without const assertions to match TS d.ts emit.
if let Some(initializer) =
self.get_literal_initializer_without_const_assertion(expr)
{
value = Some(initializer);
None
} else if Self::is_non_const_array_literal(expr) {
@@ -146,7 +148,10 @@ impl<'a> IsolatedDeclarations<'a> {
)
}
fn get_literal_const_initializer(&self, expr: &Expression<'a>) -> Option<Expression<'a>> {
fn get_literal_initializer_without_const_assertion(
&self,
expr: &Expression<'a>,
) -> Option<Expression<'a>> {
match expr {
Expression::BooleanLiteral(_)
| Expression::NumericLiteral(_)
@@ -159,13 +164,7 @@ impl<'a> IsolatedDeclarations<'a> {
Some(Expression::UnaryExpression(expr.clone_in(self.allocator())))
}
Expression::ParenthesizedExpression(expr) => {
self.get_literal_const_initializer(&expr.expression)
}
Expression::TSAsExpression(expr) if expr.type_annotation.is_const_type_reference() => {
self.get_literal_const_initializer(&expr.expression)
}
Expression::TSTypeAssertion(expr) if expr.type_annotation.is_const_type_reference() => {
self.get_literal_const_initializer(&expr.expression)
self.get_literal_initializer_without_const_assertion(&expr.expression)
}
_ => None,
}
@@ -0,0 +1,11 @@
export class C {
readonly instanceBare = "i";
readonly instanceConst = "ic" as const;
static readonly staticBare = "s";
static readonly staticConst = "sc" as const;
static readonly A = "a" as const;
static readonly B: "b" = "b";
static D = "d" as const;
}
export const bare = "e" as const;
@@ -0,0 +1,17 @@
---
source: crates/oxc_isolated_declarations/tests/mod.rs
input_file: crates/oxc_isolated_declarations/tests/fixtures/issue-24284.ts
---
```
==================== .D.TS ====================
export declare class C {
readonly instanceBare = "i";
readonly instanceConst: "ic";
static readonly staticBare = "s";
static readonly staticConst: "sc";
static readonly A: "a";
static readonly B: "b";
static D: "d";
}
export declare const bare: "e";
@@ -8,15 +8,15 @@ input_file: crates/oxc_isolated_declarations/tests/fixtures/readonly-class-prope
export declare class ReadonlyLiteralInitializers {
readonly directTrue = true;
readonly parenTrue = true;
readonly constAssertTrue = true;
readonly nestedConstAssert = true;
readonly constAssertTrue: true;
readonly nestedConstAssert: true;
readonly directString = "x";
readonly parenString = "x";
readonly templateNoExpr = "x";
readonly constAssertString = "x";
readonly constAssertString: "x";
readonly unaryNumber = -1;
readonly unaryParenNumber = -1;
readonly constAssertNumber = 1;
readonly constAssertNumber: 1;
readonly directObject: {
a: number;
};