Interesting reading your comments, I was thinking something along similar lines a while ago, I actually got a simple type mapping working in the current type system by abusing the constraint solving. The below works (on pyright, but not mypy) to give a mapping from literal keys to differently typed values. It’s a bit clunky, but doesn’t require every overload to be specified:
from __future__ import annotations
from typing import Any, Literal, LiteralString
class Mapping[Kt: tuple[Any, Any]]:
def __getitem__[GKt, Vt](
self: Mapping[tuple[GKt, Vt]],
key: GKt
) -> Vt:
...
class ApplyGet[Tk: LiteralString]:
val: Tk
def __init__(self, val: Tk):
self.val = val
def get[Vt, Vo: tuple[Any, Any]](self, mapping: Mapping[tuple[Tk, Vt] | Vo]) -> Vt:
return mapping[self.val]
type IntKey = Literal["int_key"]
type IntKV = tuple[IntKey, int]
type StrKey = Literal["str_key"]
type ErrorStr = Literal["error_key"]
type StrKV = tuple[StrKey, str]
type MyMapping = Mapping[IntKV | StrKV]
def get_val(mapping: MyMapping):
int_key: IntKey = "int_key"
int_val = ApplyGet(int_key).get(mapping)
raw_int_val = ApplyGet("int_key").get(mapping)
reveal_type(int_val) # inferred type: int - correct
reveal_type(raw_int_val) # inferred type: int - correct
str_key: StrKey = "str_key"
str_val = ApplyGet(str_key).get(mapping)
raw_str_val = ApplyGet("str_key").get(mapping)
reveal_type(str_val) # inferred type: str - correct
reveal_type(raw_str_val) # inferred type: str - correct
error_key: ErrorStr = "error_key"
error_val = ApplyGet(error_key).get(mapping) # return type unknown error - correct
raw_error_val = ApplyGet("error_key").get(mapping) # return type unknown error - correct
reveal_type(error_val) # inferred type: unknown - correct
reveal_type(raw_error_val) # inferred type: unknown - correct
As Eric pointed out, mypy fully rejects it and it relies on the behaviour of the constraint system, which is currently completely unspecified in the type system. I think pyright merges a union of tuples into a tuple of unions at 64 types to avoid combinatorial explosion. It’s a neat way to push the type system though.
What I’d like is something like a “type pair” which I think is pretty close to your type mapping, that would allow the below:
from __future__ import annotations
from typing import Any, LiteralString
type TypePair = tuple[Any, Any]
class Mapping[KvT: TypePair]:
def __getitem__[Kt, Vt, OtherT: TypePair](
self: Mapping[tuple[Kt, Vt] | OtherT],
key: Kt
) -> Vt:
...
type Mappings = tuple[int, str] | tuple[str, list[str]]
type MyMapping = Mapping[Mappings]
def use_get_fn(mapping: MyMapping):
val_for_int_key = mapping[2]
val_for_str_key= mapping["str_key"]
But without the indirection shown above pyright doesn’t resolve the constraints as might be expected (not an easy thing to do without slowing down).