[ty] support recursive type aliases (#19805)

## Summary

Support recursive type aliases by adding a `Type::TypeAlias` type
variant, which allows referring to a type alias directly as a type
without eagerly unpacking it to its value.

We still unpack type aliases when they are added to intersections and
unions, so that we can simplify the intersection/union appropriately
based on the unpacked value of the type alias.

This introduces new possible recursive types, and so also requires
expanding our usage of recursion-detecting visitors in Type methods. The
use of these visitors is still not fully comprehensive in this PR, and
will require further expansion to support recursion in more kinds of
types (I already have further work on this locally), but I think it may
be better to do this incrementally in multiple PRs.

## Test Plan

Added some recursive type-alias tests and made them pass.
This commit is contained in:
Carl Meyer 2025-08-12 09:03:10 -07:00 committed by GitHub
parent d76fd103ae
commit 13bdba5d28
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
19 changed files with 542 additions and 150 deletions

View file

@ -71,6 +71,18 @@ type ListOrSet[T] = list[T] | set[T]
reveal_type(ListOrSet.__type_params__) # revealed: tuple[TypeVar | ParamSpec | TypeVarTuple, ...]
```
## In unions and intersections
We can "break apart" a type alias by e.g. adding it to a union:
```py
type IntOrStr = int | str
def f(x: IntOrStr, y: str | bytes):
z = x or y
reveal_type(z) # revealed: (int & ~AlwaysFalsy) | str | bytes
```
## `TypeAliasType` properties
Two `TypeAliasType`s are distinct and disjoint, even if they refer to the same type
@ -138,3 +150,50 @@ def get_name() -> str:
# error: [invalid-type-alias-type] "The name of a `typing.TypeAlias` must be a string literal"
IntOrStr = TypeAliasType(get_name(), int | str)
```
## Cyclic aliases
### Self-referential
```py
type OptNestedInt = int | tuple[OptNestedInt, ...] | None
def f(x: OptNestedInt) -> None:
reveal_type(x) # revealed: int | tuple[OptNestedInt, ...] | None
if x is not None:
reveal_type(x) # revealed: int | tuple[OptNestedInt, ...]
```
### Invalid self-referential
```py
# TODO emit a diagnostic here
type IntOr = int | IntOr
def f(x: IntOr):
reveal_type(x) # revealed: int
if not isinstance(x, int):
reveal_type(x) # revealed: Never
```
### Mutually recursive
```py
type A = tuple[B] | None
type B = tuple[A] | None
def f(x: A):
if x is not None:
reveal_type(x) # revealed: tuple[B]
y = x[0]
if y is not None:
reveal_type(y) # revealed: tuple[A]
def g(x: A | B):
reveal_type(x) # revealed: tuple[B] | None
from ty_extensions import Intersection
def h(x: Intersection[A, B]):
reveal_type(x) # revealed: tuple[B] | None
```