Best practices for type hinting/checking (e.g. with `Pyright`)?

What are the best practices for type hinting so that type checkers can understand and provide static type checking?

This is partially a question, but more generally intended as a place to discuss type hinting/checking with reflex.

I’ve often been tripped up by type-checking related to state vars and event handlers etc.
I see that in v0.6.3 some additions were made to help with this. Namely rx.Field, rx.field, and @rx.event. I think these will help greatly going forward.

The first example below uses @rx.event to solve some type checking issues I’ve had in the past, particularly in the get_component method of rx.ComponentState classes.

The second example is more of a question about when/how to use rx.Field/field. It seems to me that it creates as many type hinting issues as it fixes. (not technically create issues, but add boilerplate for little gain).


Example 1 – type hinting in event handlers



class ExampleComponent(rx.ComponentState):
    some_int: int = 0

    @rx.event  # Adding this fixes issues in get_component
    def do_something(self) -> None:
        self.some_int += 1

    @rx.event  # Adding this fixes issues in get_component
    def do_something_with_arg(self, some_arg: str) -> None:
        self.some_int += len(some_arg)

    @classmethod
    def get_component(cls, *children, **props) -> rx.Component:
        # This silences `unresolved attribute...` on `cls.some_int` when using pydantic plugin in PyCharm
        # noinspection Pydantic
        return rx.box(
            rx.text(cls.some_int),
            # Issue fixed -- Argument of type "(self: Self@ExampleComponent) -> None" cannot be assigned
            rx.button("do something", on_click=cls.do_something),
            # Issue fixed -- Argument missing for parameter "some_arg"
            rx.button(
                "do something with arg", on_click=cls.do_something_with_arg("arg")
            ),
            *children,
            **props,
        )

In the above, the @rx.event decorator very nicely solves the previous issues.

It does it in a clever way too, such that trying to assign cls.do_something_with_arg("arg", "incorrect second arg") as the event handler will raise a type checking error about passing a second argument when the event handler method is only expecting one.
So that all seems great.


Example 2 – Type hinting in component layout helper methods

def requires_int(must_be_int: int) -> str:
    return str(must_be_int)


def requires_int_var(must_be_int_var: rx.Var[int]) -> str:
    return str(must_be_int_var)


class RegularState(rx.State):
    some_int: int = 0
    int_with_field: rx.Field[int] = rx.field(0)


def index() -> rx.Component:
    return rx.box(
        # rx.Field notation not required for this
        rx.text(requires_int(RegularState.some_int)),  # This is OK
        rx.text(
            requires_int(RegularState.int_with_field)
        ),  # Argument of type "NumberVar[Unknown]" cannot ...
        rx.text(requires_int_var(RegularState.int_with_field)),  # This is OK
        #
        rx.text(
            RegularState.some_int.to(str)
        ),  # Cannot access attribute "to" for class "int"
        rx.text(RegularState.int_with_field.to(str)),  # This is OK
    )


The some_int field is annotated how I would have done before, and int_with_field is using the new rx.Field and rx.field to more accurately type hint that this is actually going to be an rx.Var[int].

Now, when passing state vars to helper rendering functions I get type checking errors if the helper function expects a regular int where previously I wouldn’t have (e.g. using .some_int) . Ultimately, this does make sense since the value really will be an rx.Var that just happens to behave like an int in many cases. So, I guess the idea is that I should just be explicit about expecting rx.Var[int] now, (or a union if I want to be able to re-use the same function with actual ints)?

Would it be nice for the rx.Field to leave make the type look like int | rx.Var[int] itself here? (that was the behaviour I was expecting, but not for any great reason)

In fact, I already did specify unions of e.g. int | rx.Var[int] as the arg type whenever I really did want to use rx.Var specific behaviour in my helper functions, so I’m not sure what I really gain here by using rx.Field in the state itself.

Of course, using the new rx.Field also allows the type checker to recognize the rx.Var methods (like .to(...)). But again, I use to get around that by specifying unions of int | rx.Var[int] in any helper method where I would want that behaviour.

Don’t get me wrong, I am happy to have the option for more specific typing, but am I missing something about the helpfulness of the new rx.Field?


Those are my thoughts for now. I’d be interested to hear how other people feel about type hinting when working with reflex.

2 Likes