# Generic type aliases: PEP 695 syntax

```toml
[environment]
python-version = "3.13"
```

## Defining a generic alias

At its simplest, to define a type alias using PEP 695 syntax, you add a list of `TypeVar`s,
`ParamSpec`s or `TypeVarTuple`s after the alias name.

```py
from ty_extensions import generic_context

type SingleTypevar[T] = ...
type MultipleTypevars[T, S] = ...
type SingleParamSpec[**P] = ...
type TypeVarAndParamSpec[T, **P] = ...
type SingleTypeVarTuple[*Ts] = ...
type TypeVarAndTypeVarTuple[T, *Ts] = ...

# revealed: ty_extensions.GenericContext[T@SingleTypevar]
reveal_type(generic_context(SingleTypevar))
# revealed: ty_extensions.GenericContext[T@MultipleTypevars, S@MultipleTypevars]
reveal_type(generic_context(MultipleTypevars))

# TODO: support `TypeVarTuple` properly
# (these should include the `TypeVarTuple`s in their generic contexts)
# revealed: ty_extensions.GenericContext[P@SingleParamSpec]
reveal_type(generic_context(SingleParamSpec))
# revealed: ty_extensions.GenericContext[T@TypeVarAndParamSpec, P@TypeVarAndParamSpec]
reveal_type(generic_context(TypeVarAndParamSpec))
# revealed: ty_extensions.GenericContext[]
reveal_type(generic_context(SingleTypeVarTuple))
# revealed: ty_extensions.GenericContext[T@TypeVarAndTypeVarTuple]
reveal_type(generic_context(TypeVarAndTypeVarTuple))
```

You cannot use the same typevar more than once.

```py
# error: [invalid-syntax] "duplicate type parameter"
type RepeatedTypevar[T, T] = ...
```

## Specializing type aliases explicitly

The type parameter can be specified explicitly:

```py
from typing import Literal

type C[T] = T

def _(a: C[int], b: C[Literal[5]]):
    reveal_type(a)  # revealed: int
    reveal_type(b)  # revealed: Literal[5]
```

The specialization must match the generic types:

```py
# error: [invalid-type-arguments] "Too many type arguments: expected 1, got 2"
reveal_type(C[int, int])  # revealed: <type alias 'C[Unknown]'>
```

And non-generic types cannot be specialized:

```py
from typing import TypeVar, Protocol, TypedDict

type B = ...

# error: [not-subscriptable] "Cannot subscript non-generic type alias"
reveal_type(B[int])  # revealed: Unknown

# error: [not-subscriptable] "Cannot subscript non-generic type alias"
def _(b: B[int]):
    reveal_type(b)  # revealed: Unknown

type IntOrStr = int | str

# error: [not-subscriptable] "Cannot subscript non-generic type alias"
def _(c: IntOrStr[int]):
    reveal_type(c)  # revealed: Unknown

type ListOfInts = list[int]

# error: [not-subscriptable] "Cannot subscript non-generic type alias: `list[int]` is already specialized"
def _(l: ListOfInts[int]):
    reveal_type(l)  # revealed: Unknown

type List[T] = list[T]

# error: [not-subscriptable] "Cannot subscript non-generic type alias: Double specialization is not allowed"
def _(l: List[int][int]):
    reveal_type(l)  # revealed: Unknown

# error: [not-subscriptable] "Cannot subscript non-generic type: `<class 'list[T@DoubleSpecialization]'>` is already specialized"
type DoubleSpecialization[T] = list[T][T]

def _(d: DoubleSpecialization[int]):
    reveal_type(d)  # revealed: Unknown

type Tuple = tuple[int, str]

# error: [not-subscriptable] "Cannot subscript non-generic type alias: `tuple[int, str]` is already specialized"
def _(doubly_specialized: Tuple[int]):
    reveal_type(doubly_specialized)  # revealed: Unknown

T = TypeVar("T")

class LegacyProto(Protocol[T]):
    pass

type LegacyProtoInt = LegacyProto[int]

# error: [not-subscriptable] "Cannot subscript non-generic type alias: `LegacyProto[int]` is already specialized"
def _(x: LegacyProtoInt[int]):
    reveal_type(x)  # revealed: Unknown

class Proto[T](Protocol):
    pass

type ProtoInt = Proto[int]

# error: [not-subscriptable] "Cannot subscript non-generic type alias: `Proto[int]` is already specialized"
def _(x: ProtoInt[int]):
    reveal_type(x)  # revealed: Unknown

# TODO: TypedDict is just a function object at runtime, we should emit an error
class LegacyDict(TypedDict[T]):
    x: T

type LegacyDictInt = LegacyDict[int]

# error: [not-subscriptable] "Cannot subscript non-generic type alias"
def _(x: LegacyDictInt[int]):
    reveal_type(x)  # revealed: Unknown

class Dict[T](TypedDict):
    x: T

type DictInt = Dict[int]

# error: [not-subscriptable] "Cannot subscript non-generic type alias: `Dict[int]` is already specialized"
def _(x: DictInt[int]):
    reveal_type(x)  # revealed: Unknown

type Union = list[str] | list[int]

# error: [not-subscriptable] "Cannot subscript non-generic type alias: `list[str] | list[int]` is already specialized"
def _(x: Union[int]):
    reveal_type(x)  # revealed: Unknown
```

If the type variable has an upper bound, the specialized type must satisfy that bound:

```py
type Bounded[T: int] = ...
type BoundedByUnion[T: int | str] = ...

class IntSubclass(int): ...

reveal_type(Bounded[int])  # revealed: <type alias 'Bounded[int]'>
reveal_type(Bounded[IntSubclass])  # revealed: <type alias 'Bounded[IntSubclass]'>

# error: [invalid-type-arguments] "Type `str` is not assignable to upper bound `int` of type variable `T@Bounded`"
reveal_type(Bounded[str])  # revealed: <type alias 'Bounded[Unknown]'>

# error: [invalid-type-arguments] "Type `int | str` is not assignable to upper bound `int` of type variable `T@Bounded`"
reveal_type(Bounded[int | str])  # revealed: <type alias 'Bounded[Unknown]'>

reveal_type(BoundedByUnion[int])  # revealed: <type alias 'BoundedByUnion[int]'>
reveal_type(BoundedByUnion[IntSubclass])  # revealed: <type alias 'BoundedByUnion[IntSubclass]'>
reveal_type(BoundedByUnion[str])  # revealed: <type alias 'BoundedByUnion[str]'>
reveal_type(BoundedByUnion[int | str])  # revealed: <type alias 'BoundedByUnion[int | str]'>

type TupleOfIntAndStr[T: int, U: str] = tuple[T, U]

def _(x: TupleOfIntAndStr[int, str]):
    reveal_type(x)  # revealed: tuple[int, str]

# error: [invalid-type-arguments] "Type `int` is not assignable to upper bound `str` of type variable `U@TupleOfIntAndStr`"
def _(x: TupleOfIntAndStr[int, int]):
    reveal_type(x)  # revealed: tuple[int, Unknown]
```

If the type variable is constrained, the specialized type must satisfy those constraints:

```py
type Constrained[T: (int, str)] = ...

reveal_type(Constrained[int])  # revealed: <type alias 'Constrained[int]'>

# TODO: error: [invalid-argument-type]
# TODO: revealed: Constrained[Unknown]
reveal_type(Constrained[IntSubclass])  # revealed: <type alias 'Constrained[IntSubclass]'>

reveal_type(Constrained[str])  # revealed: <type alias 'Constrained[str]'>

# TODO: error: [invalid-argument-type]
# TODO: revealed: Unknown
reveal_type(Constrained[int | str])  # revealed: <type alias 'Constrained[int | str]'>

# error: [invalid-type-arguments] "Type `object` does not satisfy constraints `int`, `str` of type variable `T@Constrained`"
reveal_type(Constrained[object])  # revealed: <type alias 'Constrained[Unknown]'>

type TupleOfIntOrStr[T: (int, str), U: (int, str)] = tuple[T, U]

def _(x: TupleOfIntOrStr[int, str]):
    reveal_type(x)  # revealed: tuple[int, str]

# error: [invalid-type-arguments] "Type `object` does not satisfy constraints `int`, `str` of type variable `U@TupleOfIntOrStr`"
def _(x: TupleOfIntOrStr[int, object]):
    reveal_type(x)  # revealed: tuple[int, Unknown]
```

If the type variable has a default, it can be omitted:

```py
type WithDefault[T, U = int] = ...

reveal_type(WithDefault[str, str])  # revealed: <type alias 'WithDefault[str, str]'>
reveal_type(WithDefault[str])  # revealed: <type alias 'WithDefault[str, int]'>
```

If the type alias is not specialized explicitly, it is implicitly specialized to `Unknown`:

```py
type G[T] = list[T]

def _(g: G):
    reveal_type(g)  # revealed: list[Unknown]
```

Unless a type default was provided:

```py
type G[T = int] = list[T]

def _(g: G):
    reveal_type(g)  # revealed: list[int]
```

## Aliases are not callable

```py
type A = int
type B[T] = T

# error: [call-non-callable] "Object of type `TypeAliasType` is not callable"
reveal_type(A())  # revealed: Unknown

# error: [call-non-callable] "Object of type `GenericAlias` is not callable"
reveal_type(B[int]())  # revealed: Unknown
```

## Recursive Truthiness

Make sure we handle cycles correctly when computing the truthiness of a generic type alias:

```py
type X[T: X] = T

def _(x: X):
    assert x
```

## Recursive generic type aliases

```py
type RecursiveList[T] = T | list[RecursiveList[T]]

r1: RecursiveList[int] = 1
r2: RecursiveList[int] = [1, [1, 2, 3]]
# error: [invalid-assignment] "Object of type `Literal["a"]` is not assignable to `RecursiveList[int]`"
r3: RecursiveList[int] = "a"
# error: [invalid-assignment]
r4: RecursiveList[int] = ["a"]
# TODO: this should be an error
r5: RecursiveList[int] = [1, ["a"]]

def _(x: RecursiveList[int]):
    if isinstance(x, list):
        # TODO: should be `list[RecursiveList[int]]
        reveal_type(x[0])  # revealed: int | list[Any]
    if isinstance(x, list) and isinstance(x[0], list):
        # TODO: should be `list[RecursiveList[int]]`
        reveal_type(x[0])  # revealed: list[Any]
```

Assignment checks respect structural subtyping, i.e. type aliases with the same structure are
assignable to each other.

```py
# This is structurally equivalent to RecursiveList[T].
type RecursiveList2[T] = T | list[T | list[RecursiveList[T]]]
# This is not structurally equivalent to RecursiveList[T].
type RecursiveList3[T] = T | list[list[RecursiveList[T]]]

def _(x: RecursiveList[int], y: RecursiveList2[int]):
    r1: RecursiveList2[int] = x
    # error: [invalid-assignment]
    r2: RecursiveList3[int] = x

    r3: RecursiveList[int] = y
    # error: [invalid-assignment]
    r4: RecursiveList3[int] = y
```

It is also possible to handle divergent type aliases that are not actually have instances.

```py
# The type variable `T` has no meaning here, it's just to make sure it works correctly.
type DivergentList[T] = list[DivergentList[T]]

d1: DivergentList[int] = []
# error: [invalid-assignment]
d2: DivergentList[int] = [1]
# error: [invalid-assignment]
d3: DivergentList[int] = ["a"]
# TODO: this should be an error
d4: DivergentList[int] = [[1]]

def _(x: DivergentList[int]):
    d1: DivergentList[int] = [x]
    d2: DivergentList[int] = x[0]
```

## Solving generics with type alias parameters

A generic function parameter annotated with a PEP 695 type alias that contains a type variable
should properly infer the specialization from the argument:

```py
type MyList[T] = list[T]
type MyDict[K, V] = dict[K, V]

def head[T](my_list: MyList[T]) -> T:
    return my_list[0]

def get_value[K, V](my_dict: MyDict[K, V], key: K) -> V:
    return my_dict[key]

reveal_type(head([1, 2]))  # revealed: Unknown | int
reveal_type(head(["a", "b"]))  # revealed: Unknown | str

d: dict[str, int] = {"a": 1}
reveal_type(get_value(d, "a"))  # revealed: int
```

It also works in the reverse direction, where the type alias is used as the argument type:

```py
type MyList[T] = list[T]

def head[T](l: list[T]) -> T:
    return l[0]

def _(x: MyList[int]):
    reveal_type(head(x))  # revealed: int
```

## Bidirectional type inference with union type aliases

When a PEP 695 type alias expands to a union, bidirectional type inference should still work
correctly. The type alias should be expanded to its value type when determining the expected type
for specialization inference.

```py
type MaybeList[T] = list[T] | T

def test[X: int](items: list[X]) -> list[X]:
    # The annotation MaybeList[str | int] expands to `list[str | int] | str | int`.
    # Bidirectional inference should infer list[str | int] from the list() call.
    a: MaybeList[str | int] = list(items)
    # The revealed type is list[str | int] because that's what list() returns.
    reveal_type(a)  # revealed: list[str | int]
    return items
```

This also works for more complex cases with multiple generic functions:

```py
type OptionalList[T] = list[T] | None

def copy_list[T](items: list[T]) -> list[T]:
    return list(items)

def _(values: list[int]) -> OptionalList[int]:
    result: OptionalList[int] = copy_list(values)
    # The revealed type is list[int] because that's what copy_list returns.
    reveal_type(result)  # revealed: list[int]
    return result
```

Union type aliases also work correctly with TypedDict dict literal inference:

```py
from typing import TypedDict

class Person(TypedDict):
    name: str
    age: int

type MaybePerson = Person | None

def _(p: MaybePerson):
    # Dict literal should be inferred as Person, not dict[str, str | int]
    x: MaybePerson = {"name": "Alice", "age": 30}
    reveal_type(x)  # revealed: Person
```

And with `dict()` calls in TypedDict context:

```py
from typing import TypedDict

class Dog(TypedDict):
    name: str
    breed: str

type MaybeDog = Dog | None

def _():
    # dict() call with keyword args should be inferred as Dog
    animal: MaybeDog = dict(name="Buddy", breed="Labrador")
    reveal_type(animal)  # revealed: Dog
```

And with set literal inference:

```py
type MaybeSet[T] = set[T] | T

def _():
    # Set literal should be inferred as set[int]
    x: MaybeSet[int] = {1, 2, 3}
    reveal_type(x)  # revealed: set[int]
```

## Fully qualified type alias names in error messages

When two type aliases have the same name but are in different scopes, they should be fully qualified
in error messages to distinguish them:

```py
class A:
    class B[T]:
        pass

    type D = list[int]

class C:
    class B[T]:
        pass

    type D = list[str]

def f(b: C.B[C.D]) -> None:
    # error: [invalid-assignment] "Object of type `mdtest_snippet.C.B[mdtest_snippet.C.D]` is not assignable to `mdtest_snippet.A.B[mdtest_snippet.A.D]`"
    a: A.B[A.D] = b
```

## Fully qualified type alias names in nested classes

Type aliases in nested classes should include the full class path:

```py
class Outer1:
    class Inner:
        type Alias = int

class Outer2:
    class Inner:
        type Alias = str

def g(x: Outer1.Inner.Alias, y: Outer2.Inner.Alias) -> None:
    # error: [invalid-assignment] "Object of type `mdtest_snippet.Outer2.Inner.Alias` is not assignable to `mdtest_snippet.Outer1.Inner.Alias`"
    a: Outer1.Inner.Alias = y
```

## Fully qualified generic type aliases

Ambiguous generic type aliases should also be fully qualified:

```py
class X:
    type GenAlias[T] = list[T]

class Y:
    type GenAlias[T] = dict[str, T]

def h(x: X.GenAlias[int], y: Y.GenAlias[int]) -> None:
    # error: [invalid-assignment] "Object of type `mdtest_snippet.Y.GenAlias[int]` is not assignable to `mdtest_snippet.X.GenAlias[int]`"
    a: X.GenAlias[int] = y
```

## Non-ambiguous type aliases should not be qualified

Type aliases with unique names should NOT be qualified:

```py
class P:
    type UniqueAlias1 = int

class Q:
    type UniqueAlias2 = str

def i(x: P.UniqueAlias1, y: Q.UniqueAlias2) -> None:
    # error: [invalid-assignment] "Object of type `UniqueAlias2` is not assignable to `UniqueAlias1`"
    a: P.UniqueAlias1 = y
```

## Class and type alias with same name

When a class and a type alias have the same name in different scopes, both should be fully qualified
to distinguish them in error messages:

```py
class Container1:
    class Item:
        pass

class Container2:
    type Item = str

def j(x: Container1.Item, y: Container2.Item) -> None:
    # error: [invalid-assignment] "Object of type `mdtest_snippet.Container2.Item` is not assignable to `mdtest_snippet.Container1.Item`"
    a: Container1.Item = y
```
