Bit more thought and comparison with typescripts functions. Assuming it were possible to constrain a type parameter to a Literal
type, then with a mapping class like the below and typing to match, the desired functionality could be done as described at the end
from typing import Any, assert_type, Literal
type KVT = tuple[Literal[str], Any]
class Mapping[KvT: KVT]:
def __getitem__[Kt: Literal[str], Vt, OtherT: KVT](
self: Mapping[tuple[Kt, Vt] | OtherT],
key: Kt
) -> Vt:
...
type IntKey = Literal["a", "b", "c"]
type IntKV = tuple[IntKey, int]
type StrKey = Literal["d", "e", "f"]
type StrKV = tuple[StrKey, str]
type StrIntKey = Literal["g", "h", "i"]
type StrIntKV = tuple[StrIntKey, str | int]
type MyMapping = Mapping[IntKV | StrKV | StrIntKV]
def inspect_mapping(mapping: MyMapping):
int_a_val = mapping["a"]
assert_type(int_a_val, int)
str_d_val = mapping["d"]
assert_type(str_d_val, str)
intstr_g_val = mapping["g"]
assert_type(str_d_val, str | int)
There has been some discussion about how to implement for example typescript’s Partial
. This could be typed using typing.NotRequired
applied to keys. The below wouldn’t type check but hopefully makes sense.
type Partial[M: Mapping[tuple[TKey, TVal]] = Mapping[tuple[NotRequired[TKey], TVal]]
type MyPartialMapping = Partial[MyMapping]
and Pick
type Pick[M: Mapping[tuple[TPicked, TPVal] | TRest], TPicked: Literal[str]] = Mapping[tuple[TPicked, TPVal]]
type MyIntOnlyMapping = Pick[MyMapping, IntKey]
assert_type(MyIntOnlyMapping , Mapping[tuple[Literal["a", "b", "c"], int]])
Omit
would be similar
type Omit[M: Mapping[tuple[TOmit, Any] | TKept], TOmit: Literal[str]] = Mapping[TKept]
type MyOmitNonIntMapping = Omit[MyMapping, StrKey | StrIntKey]
assert_type(MyOmitNonIntMapping, MyIntOnlyMapping)
Required
is similar to Partial
:
type Required[M: Mapping[tuple[TKey, TVal]] = Mapping[tuple[Required[TKey], TVal]]
# Assumes the below
assert_type(Required[NotRequired[Literal["a"]]], Required[Literal["a"])
So far I’ve talked about string keys, but as tuple
is a mapping the below would be an example of specifying tuple’s mapping interface
type MyTuple = tuple[int, int, str, int | str]
type MyTupleMapping = Mapping[tuple[Literal[0,1], int] | tuple[Literal[2], str] | tuple[Literal[3, int | str]]
And for one last extreme aside, a 2 by 2 array (numpy?)
type TwoByTwo = Mapping[tuple[Literal[0,1], Literal[0,1], int]]
Last update: With a little coersion pyright can as is use the above mapping type to infer value types from mapped literal keys.
class GetterFn[Tk]:
@staticmethod
def get[Tv, To](key: Tk, mapping: Mapping[tuple[Tk, Tv ] | To]) -> Tv:
...
def use_get_fn(mapping: IntStrMapping):
int_val = GetterFn[IntKey].get("int_arg", mapping)
reveal_type(int_val) # inferred type: int - correct
str_val = GetterFn[StrKey].get("str_arg", mapping)
reveal_type(str_val) # inferred type: str - correct
So correct mapping types could be achieved if pyright was less eagerly merging tuple types. If a TypedDict can be transformed to this mapped type representation and key/value types can be manipulated as above then the rest follows using existing typing.NotRequired
and typing.Required
.
Practically speaking what you’d need to manipulate these is Map
, Unpack
, GetN
, and Zip
. For comparison, if you had a tuple of tuples of int/str and wanted to pass the first value in each tuple to the str constructor, those are the operations you’d need. GetN
to provide a type index operator. Union of types behaves like a set (actually a bidirectional mapping depending on which key you constrain the tuple type by), typevartuple behaves like a sequence. They’re both iterable, order only has meaning in one case. I think with all of those you could also specify a lot of panda’s type interface if it was given a basic column name to column type mapping. The distinction between a mapping and ordered mapping would become important for projects like pandas though. Aim to manipulate the Literal[key]
type rather than the Mapping
type.
An alternative and simpler approach would be to allow a type
statement to be templated, similar to c++. Essentially the syntax I gave above.
Taking this to another extreme, having a way to convert classes to this typed mapping and back would mean it’s possible for dataclass_transform
operators to type the changes they make to the class. ORMs? How about typed database queries?