[ty] Support type[…] and Type[…] in implicit type aliases (#21421)

## Summary

Support `type[…]` in implicit type aliases, for example:
```py
SubclassOfInt = type[int]

reveal_type(SubclassOfInt)  # GenericAlias

def _(subclass_of_int: SubclassOfInt):
    reveal_type(subclass_of_int)  # type[int]
```

part of https://github.com/astral-sh/ty/issues/221

## Typing conformance

```diff
-specialtypes_type.py:138:5: error[type-assertion-failure] Argument does not have asserted type `type[Any]`
-specialtypes_type.py:140:5: error[type-assertion-failure] Argument does not have asserted type `type[Any]`
```

Two new tests passing ✔️ 

```diff
-specialtypes_type.py:146:1: error[unresolved-attribute] Object of type `GenericAlias` has no attribute `unknown`
```

An `TA4.unknown` attribute on a PEP 613 alias (`TA4: TypeAlias =
type[Any]`) is being accessed, and the conformance suite expects this to
be an error. Since we currently use the inferred type for these type
aliases (and possibly in the future as well), we treat this as a direct
access of the attribute on `type[Any]`, which falls back to an access on
`Any` itself, which succeeds. 🔴

```
+specialtypes_type.py:152:16: error[invalid-type-form] `typing.TypeVar` is not a generic class
+specialtypes_type.py:156:16: error[invalid-type-form] `typing.TypeVar` is not a generic class
```

New errors because we don't handle `T = TypeVar("T"); MyType = type[T];
MyType[T]` yet. Support for this is being tracked in
https://github.com/astral-sh/ty/issues/221 🔴

## Ecosystem impact

Looks mostly good, a few known problems. 

## Test Plan

New Markdown tests
This commit is contained in:
David Peter 2025-11-13 19:02:24 +01:00 committed by GitHub
parent f9cc26aa12
commit 9e80e5a3a6
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 207 additions and 8 deletions

View file

@ -66,6 +66,8 @@ IntOrAnnotated = int | Annotated[str, "meta"]
AnnotatedOrInt = Annotated[str, "meta"] | int
IntOrOptional = int | Optional[str]
OptionalOrInt = Optional[str] | int
IntOrTypeOfStr = int | type[str]
TypeOfStrOrInt = type[str] | int
reveal_type(IntOrStr) # revealed: types.UnionType
reveal_type(IntOrStrOrBytes1) # revealed: types.UnionType
@ -97,6 +99,8 @@ reveal_type(IntOrAnnotated) # revealed: types.UnionType
reveal_type(AnnotatedOrInt) # revealed: types.UnionType
reveal_type(IntOrOptional) # revealed: types.UnionType
reveal_type(OptionalOrInt) # revealed: types.UnionType
reveal_type(IntOrTypeOfStr) # revealed: types.UnionType
reveal_type(TypeOfStrOrInt) # revealed: types.UnionType
def _(
int_or_str: IntOrStr,
@ -129,6 +133,8 @@ def _(
annotated_or_int: AnnotatedOrInt,
int_or_optional: IntOrOptional,
optional_or_int: OptionalOrInt,
int_or_type_of_str: IntOrTypeOfStr,
type_of_str_or_int: TypeOfStrOrInt,
):
reveal_type(int_or_str) # revealed: int | str
reveal_type(int_or_str_or_bytes1) # revealed: int | str | bytes
@ -160,6 +166,8 @@ def _(
reveal_type(annotated_or_int) # revealed: str | int
reveal_type(int_or_optional) # revealed: int | str | None
reveal_type(optional_or_int) # revealed: str | None | int
reveal_type(int_or_type_of_str) # revealed: int | type[str]
reveal_type(type_of_str_or_int) # revealed: type[str] | int
```
If a type is unioned with itself in a value expression, the result is just that type. No
@ -599,6 +607,158 @@ def _(
reveal_type(invalid) # revealed: str | Unknown
```
## `type[…]` and `Type[…]`
### `type[…]`
We support implicit type aliases using `type[…]`:
```py
from typing import Any, Union, Protocol, TypeVar, Generic
T = TypeVar("T")
class A: ...
class B: ...
class G(Generic[T]): ...
class P(Protocol):
def method(self) -> None: ...
SubclassOfA = type[A]
SubclassOfAny = type[Any]
SubclassOfAOrB1 = type[A | B]
SubclassOfAOrB2 = type[A] | type[B]
SubclassOfAOrB3 = Union[type[A], type[B]]
SubclassOfG = type[G]
SubclassOfGInt = type[G[int]]
SubclassOfP = type[P]
reveal_type(SubclassOfA) # revealed: GenericAlias
reveal_type(SubclassOfAny) # revealed: GenericAlias
reveal_type(SubclassOfAOrB1) # revealed: GenericAlias
reveal_type(SubclassOfAOrB2) # revealed: types.UnionType
reveal_type(SubclassOfAOrB3) # revealed: types.UnionType
reveal_type(SubclassOfG) # revealed: GenericAlias
reveal_type(SubclassOfGInt) # revealed: GenericAlias
reveal_type(SubclassOfP) # revealed: GenericAlias
def _(
subclass_of_a: SubclassOfA,
subclass_of_any: SubclassOfAny,
subclass_of_a_or_b1: SubclassOfAOrB1,
subclass_of_a_or_b2: SubclassOfAOrB2,
subclass_of_a_or_b3: SubclassOfAOrB3,
subclass_of_g: SubclassOfG,
subclass_of_g_int: SubclassOfGInt,
subclass_of_p: SubclassOfP,
):
reveal_type(subclass_of_a) # revealed: type[A]
reveal_type(subclass_of_a()) # revealed: A
reveal_type(subclass_of_any) # revealed: type[Any]
reveal_type(subclass_of_any()) # revealed: Any
reveal_type(subclass_of_a_or_b1) # revealed: type[A] | type[B]
reveal_type(subclass_of_a_or_b1()) # revealed: A | B
reveal_type(subclass_of_a_or_b2) # revealed: type[A] | type[B]
reveal_type(subclass_of_a_or_b2()) # revealed: A | B
reveal_type(subclass_of_a_or_b3) # revealed: type[A] | type[B]
reveal_type(subclass_of_a_or_b3()) # revealed: A | B
reveal_type(subclass_of_g) # revealed: type[G[Unknown]]
reveal_type(subclass_of_g()) # revealed: G[Unknown]
reveal_type(subclass_of_g_int) # revealed: type[G[int]]
reveal_type(subclass_of_g_int()) # revealed: G[int]
reveal_type(subclass_of_p) # revealed: type[P]
```
Invalid uses result in diagnostics:
```py
# error: [invalid-type-form]
InvalidSubclass = type[1]
```
### `Type[…]`
The same also works for `typing.Type[…]`:
```py
from typing import Any, Union, Protocol, TypeVar, Generic, Type
T = TypeVar("T")
class A: ...
class B: ...
class G(Generic[T]): ...
class P(Protocol):
def method(self) -> None: ...
SubclassOfA = Type[A]
SubclassOfAny = Type[Any]
SubclassOfAOrB1 = Type[A | B]
SubclassOfAOrB2 = Type[A] | Type[B]
SubclassOfAOrB3 = Union[Type[A], Type[B]]
SubclassOfG = Type[G]
SubclassOfGInt = Type[G[int]]
SubclassOfP = Type[P]
reveal_type(SubclassOfA) # revealed: GenericAlias
reveal_type(SubclassOfAny) # revealed: GenericAlias
reveal_type(SubclassOfAOrB1) # revealed: GenericAlias
reveal_type(SubclassOfAOrB2) # revealed: types.UnionType
reveal_type(SubclassOfAOrB3) # revealed: types.UnionType
reveal_type(SubclassOfG) # revealed: GenericAlias
reveal_type(SubclassOfGInt) # revealed: GenericAlias
reveal_type(SubclassOfP) # revealed: GenericAlias
def _(
subclass_of_a: SubclassOfA,
subclass_of_any: SubclassOfAny,
subclass_of_a_or_b1: SubclassOfAOrB1,
subclass_of_a_or_b2: SubclassOfAOrB2,
subclass_of_a_or_b3: SubclassOfAOrB3,
subclass_of_g: SubclassOfG,
subclass_of_g_int: SubclassOfGInt,
subclass_of_p: SubclassOfP,
):
reveal_type(subclass_of_a) # revealed: type[A]
reveal_type(subclass_of_a()) # revealed: A
reveal_type(subclass_of_any) # revealed: type[Any]
reveal_type(subclass_of_any()) # revealed: Any
reveal_type(subclass_of_a_or_b1) # revealed: type[A] | type[B]
reveal_type(subclass_of_a_or_b1()) # revealed: A | B
reveal_type(subclass_of_a_or_b2) # revealed: type[A] | type[B]
reveal_type(subclass_of_a_or_b2()) # revealed: A | B
reveal_type(subclass_of_a_or_b3) # revealed: type[A] | type[B]
reveal_type(subclass_of_a_or_b3()) # revealed: A | B
reveal_type(subclass_of_g) # revealed: type[G[Unknown]]
reveal_type(subclass_of_g()) # revealed: G[Unknown]
reveal_type(subclass_of_g_int) # revealed: type[G[int]]
reveal_type(subclass_of_g_int()) # revealed: G[int]
reveal_type(subclass_of_p) # revealed: type[P]
```
Invalid uses result in diagnostics:
```py
# error: [invalid-type-form]
InvalidSubclass = Type[1]
```
## Stringified annotations?
From the [typing spec on type aliases](https://typing.python.org/en/latest/spec/aliases.html):
@ -633,15 +793,18 @@ from typing import Union
ListOfInts = list["int"]
StrOrStyle = Union[str, "Style"]
SubclassOfStyle = type["Style"]
class Style: ...
def _(
list_of_ints: ListOfInts,
str_or_style: StrOrStyle,
subclass_of_style: SubclassOfStyle,
):
reveal_type(list_of_ints) # revealed: list[int]
reveal_type(str_or_style) # revealed: str | Style
reveal_type(subclass_of_style) # revealed: type[Style]
```
## Recursive

View file

@ -149,8 +149,7 @@ from ty_extensions import reveal_mro
class Foo(type[int]): ...
# TODO: should be `tuple[<class 'Foo'>, <class 'type'>, <class 'object'>]
reveal_mro(Foo) # revealed: (<class 'Foo'>, @Todo(GenericAlias instance), <class 'object'>)
reveal_mro(Foo) # revealed: (<class 'Foo'>, <class 'type'>, <class 'object'>)
```
## Display of generic `type[]` types