As a suggested alternative to Partial, I continued exploring the idea of defining an inlined/anonymous typed dictionary syntax, and defining the concept of a typed dictionary comprehension on top of it (as inspired by this comment).
An inlined typed dictionary would be defined following the same already existing functional syntax:
# Functional, cannot be used in a type annotation:
A = TypedDict('A', {'a': int})
# Inlined, cannot use `total`/`closed`/etc arguments:
def fn() -> TypedDict[{'a': int}]: ...
Because a name cannot be specified for inlined typed dictionaries, the result of a TypedDict[...]
call should be an instance of some _InlinedTypedDict
class which is a bit unfortunate.
I haven’t encountered any issues when spec’ing this syntax (it was already done by multiple people before).
Now the idea would be to define a typed dictionary comprehension syntax, as per this discussion. I encountered some issues (both from a static and runtime perspective) that I wanted to discuss.
We introduce a KeyOf
special form, that would be evaluated to a Literal
by type checkers (and at runtime):
A = TypedDict(A, {'a': int, 'b': str})
type KeysOfA = KeyOf[A]
reveal_type(KeysOfA) # Literal['a', 'b']
With this KeyOf
operator and by allowing TypedDict
classes to be indexed [1], we would be able to mimic TS’s mapped types:
ImmutableA = TypedDict[{K: ReadOnly[A[K]] for K in KeyOf[A]}]
Taking some simpler examples:
T1 = TypedDict[{K: int for K in Literal['a', 'b']}]
T2 = TypedDict[{K: dict[str, A[K]] for K in KeyOf[A]}]
This raises two questions:
- At runtime, we should have
Literal[...]
iterable. Is this going to cause any issues? - The semantics of indexable
TypeDict
classes are unclear: ifA
hastotal=False
set, what doesdict[str, A[K]]
represents? WithK='a'
, is itdict[str, NotRequired[int]]
?NotRequired[dict[str, int]]
(if so, how does this behave at runtime to “move” the qualifier outside thedict
)? What about this comment?
The idea would also be to extend this to be used with type variables, meaning our inlined typed dictionary implementation should support further parametrization (similarly to Annotated[T, ...]
):
type Partial[TD: TypedDict] = TypedDict[{K: NotRequired[TD[K]] for K in KeyOf[TD]}]
Which raises one other question:
- Here,
TD
is a type variable instance, which means: type variables should be indexable; usable as an argument toKeyOf
(what shouldKeyOf[TD]
return?). If not possible, should we create a new type parameter with proper semantics defined? If using the PEP 695 syntax, how can they be differentiated from other type parameters?
If we want to support something similar to TS’ Omit<T, K>
:
# Option 1, not clear how this behaves at runtime:
type Omit[TD: TypedDict, Keys: Literal] = TypedDict[{K: TD[K] for K in KeyOf[TD] if K not in Keys}]
# Option 2, by allowing type level operations on literals:
type Omit[TD: TypedDict, Keys: Literal] = TypedDict[{K: TD[K] for K in KeyOf[TD] - Keys}]
- For option 2, does this mean we should allow type expressions like
Literal['a', 'b'] - Literal['a']
? What’s the type ofLiteral['a'] - Literal['a']
then?
Other misc. questions:
- When defining a typed dictionary comprehension by iterating over the keys of another typed dictionary, should we preserve the extra items/closed specification?
I’m not trying to get an answer on all the questions, but primarily wanted to ask if it is worth pursuing with this implementation, considering all the challenges described. Maybe some of them could be solved by not trying to be backwards compatible and instead we should introduce a new syntax? Maybe we shouldn’t worry too much about runtime support (but runtime type checkers will have trouble supporting such types)?
Eric proposed using
ValueOf[TD, K]
, which I simplified toTD[K]
for clarity. ↩︎