Repository: brentyi/tyro

Issue:
Tyro global config can not be used with union of dataclasses (subcommands)
## Example code : 

```python
from dataclasses import dataclass

import tyro


@dataclass
class A:
    pos1: str
    pos2: int
    flag: bool = False


@dataclass
class B:
    pos: int
    flag: bool = False


cmd = tyro.cli(
    A | B,
    config=[ # global config instead of manually configuring fields or annotated each dataclass
        tyro.conf.FlagCreatePairsOff,
        tyro.conf.PositionalRequiredArgs,
    ],
)
```

## Expected result

Tyro global configuration applies to both subcommands (tyro version == 0.9.23).

## Actual result

```python
TypeError: __main__.A | __main__.B is not a callable object
```

Hints from issue comments:
@brentyi: Thanks @emaheuxPEREN for the concise reproduction instructions!

Solution patch:
diff --git a/src/tyro/_fields.py b/src/tyro/_fields.py
index 23e8232e..111536ff 100644
--- a/src/tyro/_fields.py
+++ b/src/tyro/_fields.py
@@ -227,6 +227,8 @@ def field_list_from_type_or_callable(
     """
 
     type_info = StructTypeInfo.make(f, default_instance)
+    type_orig = f
+    del f
 
     with type_info._typevar_context:
         spec = ConstructorRegistry.get_struct_spec(type_info)
@@ -237,7 +239,7 @@ def field_list_from_type_or_callable(
                     FieldDefinition.from_field_spec(f) for f in spec.fields
                 ]
 
-            is_primitive = ConstructorRegistry._is_primitive_type(f, set())
+            is_primitive = ConstructorRegistry._is_primitive_type(type_orig, set())
             if is_primitive and support_single_arg_types:
                 with FieldDefinition.marker_context(
                     (_markers.Positional, _markers._PositionalCall)
@@ -247,20 +249,20 @@ def field_list_from_type_or_callable(
                         [
                             FieldDefinition.make(
                                 name="value",
-                                typ=f,
+                                typ=type_orig,
                                 default=default_instance,
                                 helptext="",
                             )
                         ],
                     )
-            elif not is_primitive and callable(f):
+            elif not is_primitive and callable(type_info.type):
                 return _field_list_from_function(
                     type_info.type,  # This will have typing.Annotated metadata stripped.
                     default_instance,
                     markers=type_info.markers,
                 )
 
-    return UnsupportedStructTypeMessage(f"{f} is not a valid struct type!")
+    return UnsupportedStructTypeMessage(f"{type_orig} is not a valid struct type!")
 
 
 def _field_list_from_function(


Test patch:
diff --git a/tests/test_conf.py b/tests/test_conf.py
index fcaab18c..fad5fc5d 100644
--- a/tests/test_conf.py
+++ b/tests/test_conf.py
@@ -1916,3 +1916,128 @@ def test_helptext_from_contents_off() -> None:
     assert "Comment in helptext." not in get_helptext_with_checks(
         tyro.conf.HelptextFromCommentsOff[Config]
     )
+
+
+# Tests for union types with config parameters (fix for union + config bug)
+
+
+@dataclasses.dataclass
+class UnionCommand1:
+    """First command."""
+
+    arg1: str
+    flag1: bool = False
+
+
+@dataclasses.dataclass
+class UnionCommand2:
+    """Second command."""
+
+    arg2: int
+    flag2: bool = True
+
+
+@dataclasses.dataclass
+class NestedUnionConfig:
+    """Nested config class."""
+
+    value: float = 1.0
+
+
+@dataclasses.dataclass
+class UnionCommand3:
+    """Third command with nested config."""
+
+    nested: NestedUnionConfig
+    name: str = "default"
+
+
+def test_union_with_empty_config():
+    """Union types should work with empty config tuple."""
+    result = tyro.cli(
+        Union[UnionCommand1, UnionCommand2],
+        args=["union-command1", "--arg1", "test"],
+        config=(),
+    )
+    assert result == UnionCommand1(arg1="test", flag1=False)
+
+
+def test_union_with_flag_create_pairs_off():
+    """Union types should work with FlagCreatePairsOff config."""
+    result = tyro.cli(
+        Union[UnionCommand1, UnionCommand2],
+        args=["union-command1", "--arg1", "test", "--flag1"],
+        config=(tyro.conf.FlagCreatePairsOff,),
+    )
+    assert result == UnionCommand1(arg1="test", flag1=True)
+
+
+def test_union_with_flag_conversion_off():
+    """Union types should work with FlagConversionOff config."""
+    result = tyro.cli(
+        Union[UnionCommand1, UnionCommand2],
+        args=["union-command2", "--arg2", "42", "--flag2", "False"],
+        config=(tyro.conf.FlagConversionOff,),
+    )
+    assert result == UnionCommand2(arg2=42, flag2=False)
+
+
+def test_union_with_omit_subcommand_prefixes():
+    """Union types should work with OmitSubcommandPrefixes."""
+    result = tyro.cli(
+        Union[UnionCommand1, UnionCommand2],
+        args=["union-command1", "--arg1", "test"],
+        config=(tyro.conf.OmitSubcommandPrefixes,),
+    )
+    assert result == UnionCommand1(arg1="test", flag1=False)
+
+
+def test_union_with_positional_required_args():
+    """Union types should work with PositionalRequiredArgs."""
+
+    @dataclasses.dataclass
+    class SimpleUnionCmd1:
+        required_arg: str
+
+    @dataclasses.dataclass
+    class SimpleUnionCmd2:
+        required_num: int
+
+    result = tyro.cli(
+        Union[SimpleUnionCmd1, SimpleUnionCmd2],
+        args=["simple-union-cmd1", "hello"],
+        config=(tyro.conf.PositionalRequiredArgs,),
+    )
+    assert result == SimpleUnionCmd1(required_arg="hello")
+
+
+def test_nested_union_with_config():
+    """Nested union types should work with config."""
+    result = tyro.cli(
+        Union[UnionCommand1, UnionCommand3],
+        args=["union-command3", "--nested.value", "2.5", "--name", "test"],
+        config=(tyro.conf.FlagCreatePairsOff,),
+    )
+    assert result == UnionCommand3(nested=NestedUnionConfig(value=2.5), name="test")
+
+
+def test_union_subcommand_help_with_config():
+    """Test that help text works correctly with union types and config."""
+    # This should not raise an exception
+    with pytest.raises(SystemExit):
+        tyro.cli(
+            Union[UnionCommand1, UnionCommand2],
+            args=["--help"],
+            config=(tyro.conf.FlagCreatePairsOff,),
+        )
+
+
+def test_union_subcommand_specific_help_with_config():
+    """Test that subcommand-specific help works with config."""
+    # This should not raise an exception
+    with pytest.raises(SystemExit):
+        tyro.cli(
+            Union[UnionCommand1, UnionCommand2],
+            args=["union-command1", "--help"],
+            config=(tyro.conf.FlagCreatePairsOff,),
+        )
diff --git a/tests/test_py311_generated/test_conf_generated.py b/tests/test_py311_generated/test_conf_generated.py
index 04d74d95..81efce62 100644
--- a/tests/test_py311_generated/test_conf_generated.py
+++ b/tests/test_py311_generated/test_conf_generated.py
@@ -1923,3 +1923,128 @@ def test_helptext_from_contents_off() -> None:
     assert "Comment in helptext." not in get_helptext_with_checks(
         tyro.conf.HelptextFromCommentsOff[Config]
     )
+
+
+# Tests for union types with config parameters (fix for union + config bug)
+
+
+@dataclasses.dataclass
+class UnionCommand1:
+    """First command."""
+
+    arg1: str
+    flag1: bool = False
+
+
+@dataclasses.dataclass
+class UnionCommand2:
+    """Second command."""
+
+    arg2: int
+    flag2: bool = True
+
+
+@dataclasses.dataclass
+class NestedUnionConfig:
+    """Nested config class."""
+
+    value: float = 1.0
+
+
+@dataclasses.dataclass
+class UnionCommand3:
+    """Third command with nested config."""
+
+    nested: NestedUnionConfig
+    name: str = "default"
+
+
+def test_union_with_empty_config():
+    """Union types should work with empty config tuple."""
+    result = tyro.cli(
+        UnionCommand1 | UnionCommand2,
+        args=["union-command1", "--arg1", "test"],
+        config=(),
+    )
+    assert result == UnionCommand1(arg1="test", flag1=False)
+
+
+def test_union_with_flag_create_pairs_off():
+    """Union types should work with FlagCreatePairsOff config."""
+    result = tyro.cli(
+        UnionCommand1 | UnionCommand2,
+        args=["union-command1", "--arg1", "test", "--flag1"],
+        config=(tyro.conf.FlagCreatePairsOff,),
+    )
+    assert result == UnionCommand1(arg1="test", flag1=True)
+
+
+def test_union_with_flag_conversion_off():
+    """Union types should work with FlagConversionOff config."""
+    result = tyro.cli(
+        UnionCommand1 | UnionCommand2,
+        args=["union-command2", "--arg2", "42", "--flag2", "False"],
+        config=(tyro.conf.FlagConversionOff,),
+    )
+    assert result == UnionCommand2(arg2=42, flag2=False)
+
+
+def test_union_with_omit_subcommand_prefixes():
+    """Union types should work with OmitSubcommandPrefixes."""
+    result = tyro.cli(
+        UnionCommand1 | UnionCommand2,
+        args=["union-command1", "--arg1", "test"],
+        config=(tyro.conf.OmitSubcommandPrefixes,),
+    )
+    assert result == UnionCommand1(arg1="test", flag1=False)
+
+
+def test_union_with_positional_required_args():
+    """Union types should work with PositionalRequiredArgs."""
+
+    @dataclasses.dataclass
+    class SimpleUnionCmd1:
+        required_arg: str
+
+    @dataclasses.dataclass
+    class SimpleUnionCmd2:
+        required_num: int
+
+    result = tyro.cli(
+        SimpleUnionCmd1 | SimpleUnionCmd2,
+        args=["simple-union-cmd1", "hello"],
+        config=(tyro.conf.PositionalRequiredArgs,),
+    )
+    assert result == SimpleUnionCmd1(required_arg="hello")
+
+
+def test_nested_union_with_config():
+    """Nested union types should work with config."""
+    result = tyro.cli(
+        UnionCommand1 | UnionCommand3,
+        args=["union-command3", "--nested.value", "2.5", "--name", "test"],
+        config=(tyro.conf.FlagCreatePairsOff,),
+    )
+    assert result == UnionCommand3(nested=NestedUnionConfig(value=2.5), name="test")
+
+
+def test_union_subcommand_help_with_config():
+    """Test that help text works correctly with union types and config."""
+    # This should not raise an exception
+    with pytest.raises(SystemExit):
+        tyro.cli(
+            UnionCommand1 | UnionCommand2,
+            args=["--help"],
+            config=(tyro.conf.FlagCreatePairsOff,),
+        )
+
+
+def test_union_subcommand_specific_help_with_config():
+    """Test that subcommand-specific help works with config."""
+    # This should not raise an exception
+    with pytest.raises(SystemExit):
+        tyro.cli(
+            UnionCommand1 | UnionCommand2,
+            args=["union-command1", "--help"],
+            config=(tyro.conf.FlagCreatePairsOff,),
+        )