extend-type-checking

Extend type checking

!!! abstract “”

How to use `register_type_namespace` to make forward references from downstream packages resolvable at composition time, enabling third-party types in app pipelines.

The problem

When you compose apps with +, scinexus checks that the output type of the left app is compatible with the input type of the right app. Type hints are often written as forward references — strings like "Alignment" or "PhyloNode" — to avoid circular imports. At composition time scinexus must resolve these strings to actual classes, but it only knows about its own types by default. If your package defines custom types used in app hints, scinexus cannot resolve them without help.

The solution

register_type_namespace lets a downstream package register a lazy namespace provider — a zero-argument callable that returns a dict[str, type]. When scinexus encounters an unresolved forward reference, it queries each registered provider in order until it finds a match.

```python { notest } from scinexus.typing import register_type_namespace

register_type_namespace(my_provider)


The provider is called lazily each time a name needs resolving, so the package can defer heavy imports. Providers are responsible for their own caching. Registration is idempotent: re-registering the same callable is a no-op.

<!-- [[[cog
from cog_utils import exec_codeblock

src = \
"""
from scinexus.typing import register_type_namespace

_resolution_ns = None

def _get_resolution_namespace():
    global _resolution_ns
    if _resolution_ns is not None:
        return _resolution_ns

    from cogent3.core.alignment import Alignment, SequenceCollection  # (1)!
    from cogent3.core.tree import PhyloNode
    # ... other imports ...

    _resolution_ns = {
        "Alignment": Alignment,
        "SequenceCollection": SequenceCollection,
        "PhyloNode": PhyloNode,  # (3)!
        # ... other types ...
    }
    return _resolution_ns

register_type_namespace(_get_resolution_namespace)  # (2)!
"""
exec_codeblock(src=src, admonition='???- example "How `cogent3` does it"',
annotations=["`cogent3` defines many types (`Alignment`, `PhyloNode`, `Table`, etc.) that are used as forward references in app type hints.",
"In `cogent3/app/typing.py`, a resolution namespace is built lazily and registered with `scinexus`",
'With this registration, any `scinexus` app that uses `"PhyloNode"` as a type hint will resolve correctly at composition time without the user importing `PhyloNode` explicitly.'
])
  ]]] -->
???- example "How `cogent3` does it"

    ```python { linenums="1" notest }
    from scinexus.typing import register_type_namespace

    _resolution_ns = None

    def _get_resolution_namespace():
        global _resolution_ns
        if _resolution_ns is not None:
            return _resolution_ns

        from cogent3.core.alignment import Alignment, SequenceCollection  # (1)!
        from cogent3.core.tree import PhyloNode
        # ... other imports ...

        _resolution_ns = {
            "Alignment": Alignment,
            "SequenceCollection": SequenceCollection,
            "PhyloNode": PhyloNode,  # (3)!
            # ... other types ...
        }
        return _resolution_ns

    register_type_namespace(_get_resolution_namespace)  # (2)!
    ```

    1. `cogent3` defines many types (`Alignment`, `PhyloNode`, `Table`, etc.) that are used as forward references in app type hints.
    2. In `cogent3/app/typing.py`, a resolution namespace is built lazily and registered with `scinexus`
    3. With this registration, any `scinexus` app that uses `"PhyloNode"` as a type hint will resolve correctly at composition time without the user importing `PhyloNode` explicitly.
<!-- [[[end]]] -->


## Registering your own package's types

Follow the same pattern: define a lazy provider function that imports and caches your types, then register it at module level.

```python { notest }
from scinexus.typing import register_type_namespace

_ns = None


def _get_my_types():
    global _ns
    if _ns is not None:
        return _ns

    from my_package.core import MyDataType, MyResultType

    _ns = {
        "MyDataType": MyDataType,
        "MyResultType": MyResultType,
    }
    return _ns


register_type_namespace(_get_my_types)

Place this in a module that is imported early (e.g. your package’s typing.py or __init__.py). Once registered, apps using "MyDataType" as a forward reference will resolve correctly when composed with other apps.