My understanding is that in the absence of any defined parameter type defaults, if GenericType[T1, T2]
is a valid form, then GenericType[T1]
always means GenericType[T1, Any]
(unless GenericType
is variadic).
So to support both forms, it’d need to be a special case, or you’d need a very clever default type for the second type parameter. There’s already some precedent for special cases[1] so it’s not unheard of, but of course special cases should probably be kept to a minimum.
Really the main thing the proposals in this thread are missing (including my drive-by generator-expression one) is they don’t really define the iteration over the type-members of the source type, or the aggregation of the mapped type-members back into the target type. This, IMO, is what leaves all these proposals feeling a little too “magical” or “limited”.
Hopefully an example helps demonstrate what I meant by that nonsense I just said:
class Something(TypedDict):
... # who cares what's in here, doesn't matter
type Attributes[T] = ... # "iteration over the type-members of the source type"
type IntoTypedDict[T] = ... # "aggregation of the mapped type-members back into the target type"
# Something, but all attributes are Optional now
type OptionalSomething = IntoTypedDict[Map[Optional, Attributes[Something]]]
# This loosely corresponds to something like:
result = dict(map(func, iter(source)))
# Which suggests a more "complete" generator-expression syntax than the one I previously proposed:
type OptionalSomething = IntoTypedDict[(name, T | None) for name, T in Attributes[Something]]
Making those “aggregate” and “iterate” parts explicit has a few benefits:
- It, subjectively, feels less magical.
- It’s more clear how to generalize this beyond
TypedDict
for use indataclasses
,Protocol
s, or other constructions - just define the appropriate “type aggregators” and/or “type iterators”. - It may help solve the runtime concern @Zomatree expressed with using generator-expressions, by making the “iterate” piece actually iterable at runtime, and making the “aggregate” piece actually
construct a type instance at runtime - while still remaining statically analyzable.[2]
I wish I had the time or the requisite background knowledge to even attempt to draft up a PEP for this, because I really think the generator-expression approach (with “type iterators” and “type aggregators”) is the right path forward here. After all, it’s pretty much what TypeScript is doing with its mapped types, except here it’s using one of Python’s most iconic syntax features to make it work.[3] I realize “TypeScript does it that way” isn’t enough on its own to declare an approach the right answer, but it’s certainly strong evidence that it’s a powerful, composable approach. Certainly more powerful/composable than a Map[T, U]
that only works for TypedDict
and only works with transforms that look like Transform[T]
, anyway.
e.g.
tuple[str, ...]
doesn’t mean “a tuple of a string and an instance of...
” ↩︎I’m not sure you even need to go that far - the AST of
Aggregate[Transform[T] for T in Iterate[S]]
may have everything you need for runtime type inspection? ↩︎You could also get conditional types for free, with the generator-expression’s
if
clause,Never
, and an appropriately defined “type aggregator”. ↩︎