Copilot commented on code in PR #3394:
URL: https://github.com/apache/fory/pull/3394#discussion_r2852483803


##########
compiler/fory_compiler/tests/test_typescript_codegen.py:
##########
@@ -0,0 +1,346 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+"""Tests for TypeScript code generation."""
+
+from pathlib import Path
+from textwrap import dedent
+
+from fory_compiler.frontend.fdl.lexer import Lexer
+from fory_compiler.frontend.fdl.parser import Parser
+from fory_compiler.generators.base import GeneratorOptions
+from fory_compiler.generators.typescript import TypeScriptGenerator
+from fory_compiler.ir.ast import Schema
+
+
+def parse_fdl(source: str) -> Schema:
+    return Parser(Lexer(source).tokenize()).parse()
+
+
+def generate_typescript(source: str) -> str:
+    schema = parse_fdl(source)
+    options = GeneratorOptions(output_dir=Path("/tmp"))
+    generator = TypeScriptGenerator(schema, options)
+    files = generator.generate()
+    assert len(files) == 1, f"Expected 1 file, got {len(files)}"
+    return files[0].content
+
+
+def test_typescript_enum_generation():
+    """Test that enums are properly generated."""
+    source = dedent(
+        """
+        package example;
+
+        enum Color [id=101] {
+            RED = 0;
+            GREEN = 1;
+            BLUE = 2;
+        }
+        """
+    )
+    output = generate_typescript(source)
+
+    # Check enum definition
+    assert "export enum Color" in output
+    assert "RED = 0" in output
+    assert "GREEN = 1" in output
+    assert "BLUE = 2" in output
+    assert "Type ID 101" in output
+
+
+def test_typescript_message_generation():
+    """Test that messages are properly generated as interfaces."""
+    source = dedent(
+        """
+        package example;
+
+        message Person [id=102] {
+            string name = 1;
+            int32 age = 2;
+            optional string email = 3;
+        }
+        """
+    )
+    output = generate_typescript(source)
+
+    # Check interface definition
+    assert "export interface Person" in output
+    assert "name: string;" in output
+    assert "age: number;" in output
+    assert "email: string | undefined;" in output
+    assert "Type ID 102" in output
+
+
+def test_typescript_nested_message():
+    """Test that nested messages are properly generated."""
+    source = dedent(
+        """
+        package example;
+
+        message Person [id=100] {
+            string name = 1;
+
+            message Address [id=101] {
+                string street = 1;
+                string city = 2;
+            }
+
+            Address address = 2;
+        }
+        """
+    )
+    output = generate_typescript(source)
+
+    # Check nested interface
+    assert "export interface Person" in output
+    assert "export interface Address" in output
+    assert "street: string;" in output
+    assert "city: string;" in output
+
+
+def test_typescript_nested_enum():
+    """Test that nested enums are properly generated."""
+    source = dedent(
+        """
+        package example;
+
+        message Person [id=100] {
+            string name = 1;
+
+            enum PhoneType [id=101] {
+                MOBILE = 0;
+                HOME = 1;
+            }
+        }
+        """
+    )
+    output = generate_typescript(source)
+
+    # Check nested enum
+    assert "export enum PhoneType" in output
+    assert "MOBILE = 0" in output
+    assert "HOME = 1" in output
+
+

Review Comment:
   This test verifies that nested enums are exported at the top level, but it 
doesn't test whether the registration function correctly references them. Since 
the registration function uses qualified names like `Person.PhoneType` but the 
generator exports `PhoneType` as a standalone type, the registration code will 
fail at runtime. Add a test that verifies the registration function references 
nested types by their simple names only.
   ```suggestion
   
   def test_typescript_nested_enum_registration_uses_simple_name():
       """Test that the registration function references nested enums by simple 
name."""
       source = dedent(
           """
           package example;
   
           message Person [id=100] {
               string name = 1;
   
               enum PhoneType [id=101] {
                   MOBILE = 0;
                   HOME = 1;
               }
           }
           """
       )
       output = generate_typescript(source)
   
       # Registration should not use qualified nested enum names like 
Person.PhoneType
       assert "Person.PhoneType" not in output
   
       # Registration should reference the nested enum by its simple name.
       # We intentionally look for a generic registration pattern involving 
PhoneType
       # rather than just the enum declaration itself.
       assert "PhoneType" in output
   ```



##########
compiler/fory_compiler/generators/typescript.py:
##########
@@ -0,0 +1,382 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+"""TypeScript/JavaScript code generator.
+
+Generates pure TypeScript type definitions from FDL IDL files.
+Supports messages, enums, unions, and all primitive types.
+"""
+
+from pathlib import Path
+from typing import List, Optional, Tuple
+
+from fory_compiler.generators.base import BaseGenerator, GeneratedFile
+from fory_compiler.ir.ast import (
+    Message,
+    Enum,
+    Union,
+    FieldType,
+    PrimitiveType,
+    NamedType,
+    ListType,
+    MapType,
+)
+from fory_compiler.ir.types import PrimitiveKind
+
+
+class TypeScriptGenerator(BaseGenerator):
+    """Generates TypeScript type definitions and interfaces from IDL."""
+
+    language_name = "typescript"
+    file_extension = ".ts"
+
+    # Mapping from FDL primitive types to TypeScript types
+    PRIMITIVE_MAP = {
+        PrimitiveKind.BOOL: "boolean",
+        PrimitiveKind.INT8: "number",
+        PrimitiveKind.INT16: "number",
+        PrimitiveKind.INT32: "number",
+        PrimitiveKind.VARINT32: "number",
+        PrimitiveKind.INT64: "bigint | number",
+        PrimitiveKind.VARINT64: "bigint | number",
+        PrimitiveKind.TAGGED_INT64: "bigint | number",
+        PrimitiveKind.UINT8: "number",
+        PrimitiveKind.UINT16: "number",
+        PrimitiveKind.UINT32: "number",
+        PrimitiveKind.VAR_UINT32: "number",
+        PrimitiveKind.UINT64: "bigint | number",
+        PrimitiveKind.VAR_UINT64: "bigint | number",
+        PrimitiveKind.TAGGED_UINT64: "bigint | number",
+        PrimitiveKind.FLOAT16: "number",
+        PrimitiveKind.FLOAT32: "number",
+        PrimitiveKind.FLOAT64: "number",
+        PrimitiveKind.STRING: "string",
+        PrimitiveKind.BYTES: "Uint8Array",
+        PrimitiveKind.DATE: "Date",
+        PrimitiveKind.TIMESTAMP: "Date",

Review Comment:
   The PRIMITIVE_MAP is missing mappings for three primitive types that exist 
in PrimitiveKind: BFLOAT16, DURATION, and DECIMAL. All other generators (Java, 
Go, etc.) provide mappings for these types. For TypeScript, these should map to:
   - BFLOAT16: "number" (same as FLOAT16)
   - DURATION: likely a duration representation type or number for milliseconds
   - DECIMAL: "number" or a string representation for precision
   
   Without these mappings, any FDL schema using these types will fall back to 
"any", which defeats the purpose of type-safe code generation.
   ```suggestion
           PrimitiveKind.FLOAT16: "number",
           PrimitiveKind.BFLOAT16: "number",
           PrimitiveKind.FLOAT32: "number",
           PrimitiveKind.FLOAT64: "number",
           PrimitiveKind.STRING: "string",
           PrimitiveKind.BYTES: "Uint8Array",
           PrimitiveKind.DATE: "Date",
           PrimitiveKind.TIMESTAMP: "Date",
           PrimitiveKind.DURATION: "number",
           PrimitiveKind.DECIMAL: "number",
   ```



##########
compiler/fory_compiler/tests/test_typescript_codegen.py:
##########
@@ -0,0 +1,346 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+"""Tests for TypeScript code generation."""
+
+from pathlib import Path
+from textwrap import dedent
+
+from fory_compiler.frontend.fdl.lexer import Lexer
+from fory_compiler.frontend.fdl.parser import Parser
+from fory_compiler.generators.base import GeneratorOptions
+from fory_compiler.generators.typescript import TypeScriptGenerator
+from fory_compiler.ir.ast import Schema
+
+
+def parse_fdl(source: str) -> Schema:
+    return Parser(Lexer(source).tokenize()).parse()
+
+
+def generate_typescript(source: str) -> str:
+    schema = parse_fdl(source)
+    options = GeneratorOptions(output_dir=Path("/tmp"))
+    generator = TypeScriptGenerator(schema, options)
+    files = generator.generate()
+    assert len(files) == 1, f"Expected 1 file, got {len(files)}"
+    return files[0].content
+
+
+def test_typescript_enum_generation():
+    """Test that enums are properly generated."""
+    source = dedent(
+        """
+        package example;
+
+        enum Color [id=101] {
+            RED = 0;
+            GREEN = 1;
+            BLUE = 2;
+        }
+        """
+    )
+    output = generate_typescript(source)
+
+    # Check enum definition
+    assert "export enum Color" in output
+    assert "RED = 0" in output
+    assert "GREEN = 1" in output
+    assert "BLUE = 2" in output
+    assert "Type ID 101" in output
+
+
+def test_typescript_message_generation():
+    """Test that messages are properly generated as interfaces."""
+    source = dedent(
+        """
+        package example;
+
+        message Person [id=102] {
+            string name = 1;
+            int32 age = 2;
+            optional string email = 3;
+        }
+        """
+    )
+    output = generate_typescript(source)
+
+    # Check interface definition
+    assert "export interface Person" in output
+    assert "name: string;" in output
+    assert "age: number;" in output
+    assert "email: string | undefined;" in output
+    assert "Type ID 102" in output
+
+
+def test_typescript_nested_message():
+    """Test that nested messages are properly generated."""
+    source = dedent(
+        """
+        package example;
+
+        message Person [id=100] {
+            string name = 1;
+
+            message Address [id=101] {
+                string street = 1;
+                string city = 2;
+            }
+
+            Address address = 2;
+        }
+        """
+    )
+    output = generate_typescript(source)
+
+    # Check nested interface
+    assert "export interface Person" in output
+    assert "export interface Address" in output
+    assert "street: string;" in output
+    assert "city: string;" in output
+
+
+def test_typescript_nested_enum():
+    """Test that nested enums are properly generated."""
+    source = dedent(
+        """
+        package example;
+
+        message Person [id=100] {
+            string name = 1;
+
+            enum PhoneType [id=101] {
+                MOBILE = 0;
+                HOME = 1;
+            }
+        }
+        """
+    )
+    output = generate_typescript(source)
+
+    # Check nested enum
+    assert "export enum PhoneType" in output
+    assert "MOBILE = 0" in output
+    assert "HOME = 1" in output
+
+
+def test_typescript_union_generation():
+    """Test that unions are properly generated as discriminated unions."""
+    source = dedent(
+        """
+        package example;
+
+        message Dog [id=101] {
+            string name = 1;
+            int32 bark_volume = 2;
+        }
+
+        message Cat [id=102] {
+            string name = 1;
+            int32 lives = 2;
+        }
+
+        union Animal [id=103] {
+            Dog dog = 1;
+            Cat cat = 2;
+        }
+        """
+    )
+    output = generate_typescript(source)
+
+    # Check union generation
+    assert "export enum AnimalCase" in output
+    assert "DOG = 1" in output
+    assert "CAT = 2" in output
+    assert "export type Animal" in output
+    assert "AnimalCase.DOG" in output
+    assert "AnimalCase.CAT" in output
+    assert "Type ID 103" in output
+
+
+def test_typescript_collection_types():
+    """Test that collection types are properly mapped."""
+    source = dedent(
+        """
+        package example;
+
+        message Data [id=100] {
+            repeated string items = 1;
+            map<string, int32> config = 2;
+        }
+        """
+    )
+    output = generate_typescript(source)
+
+    # Check collection types
+    assert "items: string[];" in output
+    assert "config: Record<string, number>;" in output
+
+
+def test_typescript_primitive_types():
+    """Test that all primitive types are properly mapped."""
+    source = dedent(
+        """
+        package example;
+
+        message AllTypes [id=100] {
+            bool f_bool = 1;
+            int32 f_int32 = 2;
+            int64 f_int64 = 3;
+            uint32 f_uint32 = 4;
+            uint64 f_uint64 = 5;
+            float f_float = 6;
+            double f_double = 7;
+            string f_string = 8;
+            bytes f_bytes = 9;
+        }
+        """
+    )
+    output = generate_typescript(source)
+
+    # Check type mappings (field names are converted to camelCase)
+    assert "fBool: boolean;" in output
+    assert "fInt32: number;" in output
+    assert "fInt64: bigint | number;" in output
+    assert "fUint32: number;" in output
+    assert "fUint64: bigint | number;" in output
+    assert "fFloat: number;" in output
+    assert "fDouble: number;" in output
+    assert "fString: string;" in output
+    assert "fBytes: Uint8Array;" in output
+
+
+def test_typescript_file_structure():
+    """Test that generated file has proper structure."""
+    source = dedent(
+        """
+        package example.v1;
+
+        enum Status [id=100] {
+            UNKNOWN = 0;
+            ACTIVE = 1;
+        }
+
+        message Request [id=101] {
+            string query = 1;
+        }
+
+        union Response [id=102] {
+            string result = 1;
+            string error = 2;
+        }
+        """
+    )
+    output = generate_typescript(source)
+
+    # Check license header
+    assert "Apache Software Foundation (ASF)" in output
+    assert "Licensed" in output
+
+    # Check package comment
+    assert "Package: example.v1" in output
+
+    # Check section comments
+    assert "// Enums" in output
+    assert "// Messages" in output
+    assert "// Unions" in output
+    assert "// Registration helper" in output
+
+    # Check registration function (uses last segment of package name)
+    assert "export function registerV1Types" in output
+
+
+def test_typescript_field_naming():
+    """Test that field names are converted to camelCase."""
+    source = dedent(
+        """
+        package example;
+
+        message Person [id=100] {
+            string first_name = 1;
+            string last_name = 2;
+            int32 phone_number = 3;
+        }
+        """
+    )
+    output = generate_typescript(source)
+
+    # Check camelCase conversion
+    assert "first_name:" in output or "firstName:" in output
+    assert "last_name:" in output or "lastName:" in output
+    assert "phone_number:" in output or "phoneNumber:" in output

Review Comment:
   The test assertions use 'or' to accept either snake_case or camelCase, which 
defeats the purpose of testing camelCase conversion. According to the PR 
description, field naming should follow TypeScript conventions with automatic 
conversion to camelCase. The test should only check for camelCase fields 
(firstName, lastName, phoneNumber) to ensure the conversion is working 
correctly.
   ```suggestion
       assert "firstName:" in output
       assert "lastName:" in output
       assert "phoneNumber:" in output
   ```



##########
compiler/fory_compiler/generators/typescript.py:
##########
@@ -0,0 +1,382 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+"""TypeScript/JavaScript code generator.
+
+Generates pure TypeScript type definitions from FDL IDL files.
+Supports messages, enums, unions, and all primitive types.
+"""
+
+from pathlib import Path
+from typing import List, Optional, Tuple
+
+from fory_compiler.generators.base import BaseGenerator, GeneratedFile
+from fory_compiler.ir.ast import (
+    Message,
+    Enum,
+    Union,
+    FieldType,
+    PrimitiveType,
+    NamedType,
+    ListType,
+    MapType,
+)
+from fory_compiler.ir.types import PrimitiveKind
+
+
+class TypeScriptGenerator(BaseGenerator):
+    """Generates TypeScript type definitions and interfaces from IDL."""
+
+    language_name = "typescript"
+    file_extension = ".ts"
+
+    # Mapping from FDL primitive types to TypeScript types
+    PRIMITIVE_MAP = {
+        PrimitiveKind.BOOL: "boolean",
+        PrimitiveKind.INT8: "number",
+        PrimitiveKind.INT16: "number",
+        PrimitiveKind.INT32: "number",
+        PrimitiveKind.VARINT32: "number",
+        PrimitiveKind.INT64: "bigint | number",
+        PrimitiveKind.VARINT64: "bigint | number",
+        PrimitiveKind.TAGGED_INT64: "bigint | number",
+        PrimitiveKind.UINT8: "number",
+        PrimitiveKind.UINT16: "number",
+        PrimitiveKind.UINT32: "number",
+        PrimitiveKind.VAR_UINT32: "number",
+        PrimitiveKind.UINT64: "bigint | number",
+        PrimitiveKind.VAR_UINT64: "bigint | number",
+        PrimitiveKind.TAGGED_UINT64: "bigint | number",
+        PrimitiveKind.FLOAT16: "number",
+        PrimitiveKind.FLOAT32: "number",
+        PrimitiveKind.FLOAT64: "number",
+        PrimitiveKind.STRING: "string",
+        PrimitiveKind.BYTES: "Uint8Array",
+        PrimitiveKind.DATE: "Date",
+        PrimitiveKind.TIMESTAMP: "Date",
+        PrimitiveKind.ANY: "any",
+    }
+
+    def __init__(self, *args, **kwargs):
+        super().__init__(*args, **kwargs)
+        self.indent_str = "  "  # TypeScript uses 2 spaces
+
+    def is_imported_type(self, type_def: object) -> bool:
+        """Return True if a type definition comes from an imported IDL file."""
+        schema_file = self.schema.source_file
+
+        # If there's no source file set, all types are local (not imported)
+        if not schema_file:
+            return False
+
+        location = getattr(type_def, "location", None)
+        if location is None or not location.file:
+            return False
+
+        # If the type's location matches the schema's source file, it's local
+        if schema_file == location.file:
+            return False
+
+        # Otherwise, try to resolve paths and compare
+        try:
+            return Path(location.file).resolve() != Path(schema_file).resolve()
+        except Exception:
+            # If Path resolution fails, compare as strings
+            return location.file != schema_file
+
+    def split_imported_types(
+        self, items: List[object]
+    ) -> Tuple[List[object], List[object]]:
+        imported: List[object] = []
+        local: List[object] = []
+        for item in items:
+            if self.is_imported_type(item):
+                imported.append(item)
+            else:
+                local.append(item)
+        return local, imported  # Return (local, imported) tuple

Review Comment:
   The split_imported_types method returns (local, imported) but all other 
generators in the codebase (cpp.py, go.py, java.py, python.py, rust.py) return 
(imported, local) in that order. This inconsistency could lead to bugs where 
the wrong list is used. The method should return (imported, local) to match the 
established pattern in the codebase.
   ```suggestion
           return imported, local  # Return (imported, local) tuple
   ```



##########
compiler/fory_compiler/generators/typescript.py:
##########
@@ -0,0 +1,382 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+"""TypeScript/JavaScript code generator.
+
+Generates pure TypeScript type definitions from FDL IDL files.
+Supports messages, enums, unions, and all primitive types.
+"""
+
+from pathlib import Path
+from typing import List, Optional, Tuple
+
+from fory_compiler.generators.base import BaseGenerator, GeneratedFile
+from fory_compiler.ir.ast import (
+    Message,
+    Enum,
+    Union,
+    FieldType,
+    PrimitiveType,
+    NamedType,
+    ListType,
+    MapType,
+)
+from fory_compiler.ir.types import PrimitiveKind
+
+
+class TypeScriptGenerator(BaseGenerator):
+    """Generates TypeScript type definitions and interfaces from IDL."""
+
+    language_name = "typescript"
+    file_extension = ".ts"
+
+    # Mapping from FDL primitive types to TypeScript types
+    PRIMITIVE_MAP = {
+        PrimitiveKind.BOOL: "boolean",
+        PrimitiveKind.INT8: "number",
+        PrimitiveKind.INT16: "number",
+        PrimitiveKind.INT32: "number",
+        PrimitiveKind.VARINT32: "number",
+        PrimitiveKind.INT64: "bigint | number",
+        PrimitiveKind.VARINT64: "bigint | number",
+        PrimitiveKind.TAGGED_INT64: "bigint | number",
+        PrimitiveKind.UINT8: "number",
+        PrimitiveKind.UINT16: "number",
+        PrimitiveKind.UINT32: "number",
+        PrimitiveKind.VAR_UINT32: "number",
+        PrimitiveKind.UINT64: "bigint | number",
+        PrimitiveKind.VAR_UINT64: "bigint | number",
+        PrimitiveKind.TAGGED_UINT64: "bigint | number",
+        PrimitiveKind.FLOAT16: "number",
+        PrimitiveKind.FLOAT32: "number",
+        PrimitiveKind.FLOAT64: "number",
+        PrimitiveKind.STRING: "string",
+        PrimitiveKind.BYTES: "Uint8Array",
+        PrimitiveKind.DATE: "Date",
+        PrimitiveKind.TIMESTAMP: "Date",
+        PrimitiveKind.ANY: "any",
+    }
+
+    def __init__(self, *args, **kwargs):
+        super().__init__(*args, **kwargs)
+        self.indent_str = "  "  # TypeScript uses 2 spaces
+
+    def is_imported_type(self, type_def: object) -> bool:
+        """Return True if a type definition comes from an imported IDL file."""
+        schema_file = self.schema.source_file
+
+        # If there's no source file set, all types are local (not imported)
+        if not schema_file:
+            return False
+
+        location = getattr(type_def, "location", None)
+        if location is None or not location.file:
+            return False
+
+        # If the type's location matches the schema's source file, it's local
+        if schema_file == location.file:
+            return False
+
+        # Otherwise, try to resolve paths and compare
+        try:
+            return Path(location.file).resolve() != Path(schema_file).resolve()
+        except Exception:
+            # If Path resolution fails, compare as strings
+            return location.file != schema_file
+
+    def split_imported_types(
+        self, items: List[object]
+    ) -> Tuple[List[object], List[object]]:
+        imported: List[object] = []
+        local: List[object] = []
+        for item in items:
+            if self.is_imported_type(item):
+                imported.append(item)
+            else:
+                local.append(item)
+        return local, imported  # Return (local, imported) tuple
+
+    def get_module_name(self) -> str:
+        """Get the TypeScript module name from package."""
+        if self.package:
+            # Convert package name to camelCase file name
+            parts = self.package.split(".")
+            return self.to_camel_case(parts[-1])
+        return "generated"
+
+    def generate_type(self, field_type: FieldType, nullable: bool = False) -> 
str:
+        """Generate TypeScript type string for a field type."""
+        type_str = ""
+
+        if isinstance(field_type, PrimitiveType):
+            type_str = self.PRIMITIVE_MAP.get(field_type.kind, "any")
+        elif isinstance(field_type, NamedType):
+            # Check if this NamedType matches a primitive type name
+            primitive_name = field_type.name.lower()
+            # Map common shorthand names to primitive kinds
+            shorthand_map = {
+                "float": PrimitiveKind.FLOAT32,
+                "double": PrimitiveKind.FLOAT64,
+            }
+            if primitive_name in shorthand_map:
+                type_str = 
self.PRIMITIVE_MAP.get(shorthand_map[primitive_name], "any")
+            else:
+                # Check if it matches any primitive kind directly
+                for primitive_kind, ts_type in self.PRIMITIVE_MAP.items():
+                    if primitive_kind.value == primitive_name:
+                        type_str = ts_type
+                        break
+                if not type_str:
+                    # If not a primitive, treat as a message/enum type
+                    type_str = self.to_pascal_case(field_type.name)
+        elif isinstance(field_type, ListType):
+            element_type = self.generate_type(field_type.element_type)
+            type_str = f"{element_type}[]"
+        elif isinstance(field_type, MapType):
+            key_type = self.generate_type(field_type.key_type)
+            value_type = self.generate_type(field_type.value_type)
+            type_str = f"Record<{key_type}, {value_type}>"
+        else:
+            type_str = "any"
+
+        if nullable:
+            type_str += " | undefined"
+
+        return type_str
+
+    def generate(self) -> List[GeneratedFile]:
+        """Generate TypeScript files for the schema."""
+        files = []
+        files.append(self.generate_module())
+        return files
+
+    def generate_module(self) -> GeneratedFile:
+        """Generate a TypeScript module with all types."""
+        lines = []
+
+        # License header
+        lines.append(self.get_license_header("//"))
+        lines.append("")
+
+        # Add package comment if present
+        if self.package:
+            lines.append(f"// Package: {self.package}")
+            lines.append("")
+
+        # Generate enums (top-level only)
+        local_enums, _ = self.split_imported_types(self.schema.enums)
+        if local_enums:
+            lines.append("// Enums")
+            lines.append("")
+            for enum in local_enums:
+                lines.extend(self.generate_enum(enum))
+                lines.append("")
+
+        # Generate unions (top-level only)
+        local_unions, _ = self.split_imported_types(self.schema.unions)
+        if local_unions:
+            lines.append("// Unions")
+            lines.append("")
+            for union in local_unions:
+                lines.extend(self.generate_union(union))
+                lines.append("")
+
+        # Generate messages (including nested types)
+        local_messages, _ = self.split_imported_types(self.schema.messages)
+        if local_messages:
+            lines.append("// Messages")
+            lines.append("")
+            for message in local_messages:
+                lines.extend(self.generate_message(message, indent=0))
+                lines.append("")
+
+        # Generate registration function
+        lines.extend(self.generate_registration())
+        lines.append("")
+
+        return GeneratedFile(
+            path=f"{self.get_module_name()}{self.file_extension}",
+            content="\n".join(lines),
+        )
+
+    def generate_enum(self, enum: Enum, indent: int = 0) -> List[str]:
+        """Generate a TypeScript enum."""
+        lines = []
+        ind = self.indent_str * indent
+        comment = self.format_type_id_comment(enum, f"{ind}//")
+        if comment:
+            lines.append(comment)
+
+        lines.append(f"{ind}export enum {enum.name} {{")
+        for value in enum.values:
+            stripped_name = self.strip_enum_prefix(enum.name, value.name)
+            lines.append(f"{ind}{self.indent_str}{stripped_name} = 
{value.value},")
+        lines.append(f"{ind}}}")
+
+        return lines
+
+    def generate_message(
+        self,
+        message: Message,
+        indent: int = 0,
+        parent_stack: Optional[List[Message]] = None,
+    ) -> List[str]:
+        """Generate a TypeScript interface for a message."""
+        lines = []
+        ind = self.indent_str * indent
+        lineage = (parent_stack or []) + [message]
+
+        comment = self.format_type_id_comment(message, f"{ind}//")
+        if comment:
+            lines.append(comment)
+
+        # Generate nested enums first
+        for nested_enum in message.nested_enums:
+            lines.extend(self.generate_enum(nested_enum, indent=indent))
+            lines.append("")
+
+        # Generate nested unions
+        for nested_union in message.nested_unions:
+            lines.extend(self.generate_union(nested_union, indent=indent))
+            lines.append("")
+
+        # Generate the main interface
+        lines.append(f"{ind}export interface {message.name} {{")
+
+        # Generate fields
+        for field in message.fields:
+            field_type = self.generate_type(field.field_type, 
nullable=field.optional)
+            lines.append(
+                f"{ind}{self.indent_str}{self.to_camel_case(field.name)}: 
{field_type};"
+            )
+
+        lines.append(f"{ind}}}")
+
+        # Generate nested messages
+        for nested_msg in message.nested_messages:
+            lines.append("")
+            lines.extend(
+                self.generate_message(nested_msg, indent=indent, 
parent_stack=lineage)
+            )
+
+        return lines
+
+    def generate_union(
+        self,
+        union: Union,
+        indent: int = 0,
+        parent_stack: Optional[List[Message]] = None,
+    ) -> List[str]:
+        """Generate a TypeScript discriminated union."""
+        lines = []
+        ind = self.indent_str * indent
+        union_name = union.name
+
+        comment = self.format_type_id_comment(union, f"{ind}//")
+        if comment:
+            lines.append(comment)
+
+        # Generate case enum
+        case_enum_name = f"{union_name}Case"
+        lines.append(f"{ind}export enum {case_enum_name} {{")
+        for field in union.fields:
+            field_name_upper = self.to_upper_snake_case(field.name)
+            lines.append(f"{ind}{self.indent_str}{field_name_upper} = 
{field.number},")
+        lines.append(f"{ind}}}")
+        lines.append("")
+
+        # Generate union type as discriminated union
+        union_cases = []
+        for field in union.fields:
+            field_type_str = self.generate_type(field.field_type)
+            case_value = self.to_upper_snake_case(field.name)
+            union_cases.append(
+                f"{ind}{self.indent_str}| ( {{ case: 
{case_enum_name}.{case_value}; value: {field_type_str} }} )"
+            )
+
+        lines.append(f"{ind}export type {union_name} =")
+        lines.extend(union_cases)
+        lines.append(f"{ind}{self.indent_str};")
+
+        return lines
+
+    def generate_registration(self) -> List[str]:
+        """Generate a registration function."""
+        lines = []
+        registration_name = (
+            f"register{self.to_pascal_case(self.get_module_name())}Types"
+        )
+
+        lines.append("// Registration helper")
+        lines.append(f"export function {registration_name}(fory: any): void 
{{")
+
+        # Register enums
+        for enum in self.schema.enums:
+            if self.is_imported_type(enum):
+                continue
+            if self.should_register_by_id(enum):
+                type_id = enum.type_id
+                lines.append(f"  fory.register({enum.name}, {type_id});")
+
+        # Register messages
+        for message in self.schema.messages:
+            if self.is_imported_type(message):
+                continue
+            self._generate_message_registration(message, lines)
+
+        # Register unions
+        for union in self.schema.unions:
+            if self.is_imported_type(union):
+                continue
+            if self.should_register_by_id(union):
+                type_id = union.type_id
+                lines.append(f"  fory.registerUnion({union.name}, {type_id});")
+
+        lines.append("}")
+
+        return lines
+
+    def _generate_message_registration(
+        self, message: Message, lines: List[str], parent: Optional[str] = None
+    ):
+        """Generate registration for a message and its nested types."""
+        qual_name = f"{parent}.{message.name}" if parent else message.name
+
+        # Register nested enums
+        for nested_enum in message.nested_enums:
+            if self.should_register_by_id(nested_enum):
+                type_id = nested_enum.type_id
+                lines.append(
+                    f"  fory.register({qual_name}.{nested_enum.name}, 
{type_id});"
+                )
+
+        # Register nested unions
+        for nested_union in message.nested_unions:
+            if self.should_register_by_id(nested_union):
+                type_id = nested_union.type_id
+                lines.append(
+                    f"  fory.registerUnion({qual_name}.{nested_union.name}, 
{type_id});"
+                )
+
+        # Register nested messages
+        for nested_msg in message.nested_messages:
+            self._generate_message_registration(nested_msg, lines, qual_name)
+
+        # Register the message itself
+        if self.should_register_by_id(message):
+            type_id = message.type_id
+            lines.append(f"  fory.register({qual_name}, {type_id});")

Review Comment:
   The registration function generates code that attempts to access nested 
types using qualified names like `Parent.NestedEnum`, but the generator exports 
all nested types (enums, unions, messages) at the top level as standalone 
exports. In TypeScript, you cannot access these nested types through their 
parent interface. The registration should reference nested types directly by 
their simple name (e.g., `NestedEnum` instead of `Parent.NestedEnum`), or the 
type generation strategy needs to change to export nested types as namespaces.



##########
compiler/fory_compiler/generators/typescript.py:
##########
@@ -0,0 +1,382 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+"""TypeScript/JavaScript code generator.
+
+Generates pure TypeScript type definitions from FDL IDL files.
+Supports messages, enums, unions, and all primitive types.
+"""
+
+from pathlib import Path
+from typing import List, Optional, Tuple
+
+from fory_compiler.generators.base import BaseGenerator, GeneratedFile
+from fory_compiler.ir.ast import (
+    Message,
+    Enum,
+    Union,
+    FieldType,
+    PrimitiveType,
+    NamedType,
+    ListType,
+    MapType,
+)
+from fory_compiler.ir.types import PrimitiveKind
+
+
+class TypeScriptGenerator(BaseGenerator):
+    """Generates TypeScript type definitions and interfaces from IDL."""
+
+    language_name = "typescript"
+    file_extension = ".ts"
+
+    # Mapping from FDL primitive types to TypeScript types
+    PRIMITIVE_MAP = {
+        PrimitiveKind.BOOL: "boolean",
+        PrimitiveKind.INT8: "number",
+        PrimitiveKind.INT16: "number",
+        PrimitiveKind.INT32: "number",
+        PrimitiveKind.VARINT32: "number",
+        PrimitiveKind.INT64: "bigint | number",
+        PrimitiveKind.VARINT64: "bigint | number",
+        PrimitiveKind.TAGGED_INT64: "bigint | number",
+        PrimitiveKind.UINT8: "number",
+        PrimitiveKind.UINT16: "number",
+        PrimitiveKind.UINT32: "number",
+        PrimitiveKind.VAR_UINT32: "number",
+        PrimitiveKind.UINT64: "bigint | number",
+        PrimitiveKind.VAR_UINT64: "bigint | number",
+        PrimitiveKind.TAGGED_UINT64: "bigint | number",
+        PrimitiveKind.FLOAT16: "number",
+        PrimitiveKind.FLOAT32: "number",
+        PrimitiveKind.FLOAT64: "number",
+        PrimitiveKind.STRING: "string",
+        PrimitiveKind.BYTES: "Uint8Array",
+        PrimitiveKind.DATE: "Date",
+        PrimitiveKind.TIMESTAMP: "Date",
+        PrimitiveKind.ANY: "any",
+    }
+
+    def __init__(self, *args, **kwargs):
+        super().__init__(*args, **kwargs)
+        self.indent_str = "  "  # TypeScript uses 2 spaces
+
+    def is_imported_type(self, type_def: object) -> bool:
+        """Return True if a type definition comes from an imported IDL file."""
+        schema_file = self.schema.source_file
+
+        # If there's no source file set, all types are local (not imported)
+        if not schema_file:
+            return False
+
+        location = getattr(type_def, "location", None)
+        if location is None or not location.file:
+            return False
+
+        # If the type's location matches the schema's source file, it's local
+        if schema_file == location.file:
+            return False
+
+        # Otherwise, try to resolve paths and compare
+        try:
+            return Path(location.file).resolve() != Path(schema_file).resolve()
+        except Exception:
+            # If Path resolution fails, compare as strings
+            return location.file != schema_file
+
+    def split_imported_types(
+        self, items: List[object]
+    ) -> Tuple[List[object], List[object]]:
+        imported: List[object] = []
+        local: List[object] = []
+        for item in items:
+            if self.is_imported_type(item):
+                imported.append(item)
+            else:
+                local.append(item)
+        return local, imported  # Return (local, imported) tuple
+
+    def get_module_name(self) -> str:
+        """Get the TypeScript module name from package."""
+        if self.package:
+            # Convert package name to camelCase file name
+            parts = self.package.split(".")
+            return self.to_camel_case(parts[-1])
+        return "generated"
+
+    def generate_type(self, field_type: FieldType, nullable: bool = False) -> 
str:
+        """Generate TypeScript type string for a field type."""
+        type_str = ""
+
+        if isinstance(field_type, PrimitiveType):
+            type_str = self.PRIMITIVE_MAP.get(field_type.kind, "any")
+        elif isinstance(field_type, NamedType):
+            # Check if this NamedType matches a primitive type name
+            primitive_name = field_type.name.lower()
+            # Map common shorthand names to primitive kinds
+            shorthand_map = {
+                "float": PrimitiveKind.FLOAT32,
+                "double": PrimitiveKind.FLOAT64,
+            }
+            if primitive_name in shorthand_map:
+                type_str = 
self.PRIMITIVE_MAP.get(shorthand_map[primitive_name], "any")
+            else:
+                # Check if it matches any primitive kind directly
+                for primitive_kind, ts_type in self.PRIMITIVE_MAP.items():
+                    if primitive_kind.value == primitive_name:
+                        type_str = ts_type
+                        break
+                if not type_str:
+                    # If not a primitive, treat as a message/enum type
+                    type_str = self.to_pascal_case(field_type.name)
+        elif isinstance(field_type, ListType):
+            element_type = self.generate_type(field_type.element_type)
+            type_str = f"{element_type}[]"
+        elif isinstance(field_type, MapType):
+            key_type = self.generate_type(field_type.key_type)
+            value_type = self.generate_type(field_type.value_type)
+            type_str = f"Record<{key_type}, {value_type}>"
+        else:
+            type_str = "any"
+
+        if nullable:
+            type_str += " | undefined"
+
+        return type_str
+
+    def generate(self) -> List[GeneratedFile]:
+        """Generate TypeScript files for the schema."""
+        files = []
+        files.append(self.generate_module())
+        return files
+
+    def generate_module(self) -> GeneratedFile:
+        """Generate a TypeScript module with all types."""
+        lines = []
+
+        # License header
+        lines.append(self.get_license_header("//"))
+        lines.append("")
+
+        # Add package comment if present
+        if self.package:
+            lines.append(f"// Package: {self.package}")
+            lines.append("")
+
+        # Generate enums (top-level only)
+        local_enums, _ = self.split_imported_types(self.schema.enums)
+        if local_enums:
+            lines.append("// Enums")
+            lines.append("")
+            for enum in local_enums:
+                lines.extend(self.generate_enum(enum))
+                lines.append("")
+
+        # Generate unions (top-level only)
+        local_unions, _ = self.split_imported_types(self.schema.unions)
+        if local_unions:
+            lines.append("// Unions")
+            lines.append("")
+            for union in local_unions:
+                lines.extend(self.generate_union(union))
+                lines.append("")
+
+        # Generate messages (including nested types)
+        local_messages, _ = self.split_imported_types(self.schema.messages)
+        if local_messages:
+            lines.append("// Messages")
+            lines.append("")
+            for message in local_messages:
+                lines.extend(self.generate_message(message, indent=0))
+                lines.append("")
+
+        # Generate registration function
+        lines.extend(self.generate_registration())
+        lines.append("")
+
+        return GeneratedFile(
+            path=f"{self.get_module_name()}{self.file_extension}",
+            content="\n".join(lines),
+        )
+
+    def generate_enum(self, enum: Enum, indent: int = 0) -> List[str]:
+        """Generate a TypeScript enum."""
+        lines = []
+        ind = self.indent_str * indent
+        comment = self.format_type_id_comment(enum, f"{ind}//")
+        if comment:
+            lines.append(comment)
+
+        lines.append(f"{ind}export enum {enum.name} {{")
+        for value in enum.values:
+            stripped_name = self.strip_enum_prefix(enum.name, value.name)
+            lines.append(f"{ind}{self.indent_str}{stripped_name} = 
{value.value},")
+        lines.append(f"{ind}}}")
+
+        return lines
+
+    def generate_message(
+        self,
+        message: Message,
+        indent: int = 0,
+        parent_stack: Optional[List[Message]] = None,
+    ) -> List[str]:
+        """Generate a TypeScript interface for a message."""
+        lines = []
+        ind = self.indent_str * indent
+        lineage = (parent_stack or []) + [message]
+
+        comment = self.format_type_id_comment(message, f"{ind}//")
+        if comment:
+            lines.append(comment)
+
+        # Generate nested enums first
+        for nested_enum in message.nested_enums:
+            lines.extend(self.generate_enum(nested_enum, indent=indent))
+            lines.append("")
+
+        # Generate nested unions
+        for nested_union in message.nested_unions:
+            lines.extend(self.generate_union(nested_union, indent=indent))
+            lines.append("")

Review Comment:
   Nested enums are generated before their parent interface, which means they 
will appear in the output before the parent message. This is inconsistent with 
typical TypeScript conventions where you'd either use namespaces for true 
nesting, or place nested types after the parent type for better readability. 
Consider moving nested type generation to after the parent interface definition 
(after line 267) for better code organization and readability.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]


Reply via email to