> user_projection: tuple[str] = await fetch_projection(User, id=1, fields(User))
> This can't really be very type-safe since Mypy treats `fields(User)` as `dataclasses.Field*[Any]`. Now, if Mypy treated it as `dataclasses.Field*[str]`, I assume that would be a different story, and the function could be annotated to return a 1-tuple of `str`.
Typecheckers would have to special-case `dataclasses.fields(Foo)` to return a tuple of Fields from the specific dataclass. So, `fields(User)` would have type `Tuple[Field[int], Field[str]]`. In your example, `fields(User)` would have type `Field[str]`.
This approach would work when projecting a single field or a fixed number of fields.
To support projecting *arbitrary* numbers of fields, we can use variadic tuples (PEP 646 ) and the `Map` operator (to be introduced in a follow-up PEP).
As a concrete example, sqlalchemy's `Session.query` accepts arbitrary columns (or classes) and returns a Query object. A Query object is basically an iterator of tuples.
Example of `query` from the sqlalchemy docs :
__tablename__ = 'users'
id = Column(Integer, Sequence('user_id_seq'), primary_key=True)
name = Column(String(50))
fullname = Column(String(50))
nickname = Column(String(50))
>>> for name, fullname in session.query(User.name, User.fullname):
... print(name, fullname)
ed Ed Jones
wendy Wendy Williams
mary Mary Contrary
fred Fred Flintstone
We could type the `query` function as follows:
# Generic alias to capture the type of a class or a column (i.e., a field).
ClassOrColumn = Type[T] | Column[T]
Ts = TypeVarTuple("Ts")
def query(self, *args: *Map[ClassOrColumn, Ts]) -> Query[Tuple[*Ts]]: ...
# => Column[str]
# actually Column[Optional[str]], but keeping it simple here
# => Column[str]
# => Query[Tuple[str, str]]
# For the above function call, the `query` function behaves as if it were the following:
def query(self, entity1: ClassOrColumn[T1], entity2: ClassOrColumn[T2]) -> Query[Tuple[T1, T2]]: ...
+ Because `query` is given two arguments, `Ts` is seen as a tuple of two TypeVars: `Tuple[T1, T2]`.
+ The `Map[ClassOrColumn, Ts]` maps `ClassOrColumn` over each element of `Tuple[T1, T2]` to give `Tuple[ClassOrColumn[T1], ClassOrColumn[T2]]`.
+ Finally, using `*args: *<some_tuple>` means that it will accept arguments corresponding to the tuple.
+ So, `*args: *Tuple[ClassOrColumn[T1], ClassOrColumn[T2]]` means it will accept two arguments, one of type `ClassOrColumn[T1]` and another of `ClassOrColumn[T2]`.
+ That binds `T1` to `str` and `T2` to `str`, giving a return type of `Query[Tuple[str, str]]`.
Other examples follow similarly:
session.query(User, User.name, User.fullname)
# => Query[Tuple[User, str, str]]
We can adapt the above approach to the ORM/ODM projection functions you had in mind. But, first, both PEP 646 and the yet-to-be-submitted `Map` PEP have to be accepted :)
If you're interested in these developments, you could attend the monthly "Tensor typing" meetings announced on this list or read the meeting minutes .
Note that `query` omits the tuple when there is just one argument. So, we'd need an `overload` for that case.