Skip to content

Feature: Recursive type aliases #60

Closed
Closed

Description

@yangdanny97

We need to change the way we represent type aliases to be more like classes, to handle recursive aliases. Right the aliased type is stored inline, making recursion impossible; all recursive references are treated as Unknown.

This can be solved with a layer of indirection - we could have TypeAlias store a unique identifier, and look up the aliased type somewhere else. This would be similar to how we have a ClassType and use it to look up the ClassMetadata. Because of this indirection, we would need to store type arguments for any instantiated type aliases so that we can use it to instantiate the forall after looking up the type - the overall structure is very similar to Class v.s. ClassType.

This indirection would solve the problem for type aliases created with TypeAlias or the type statement.

There are two tricky parts here, related to how we extend the solution to implicit type aliases. Right now, we decide whether something is an implicit type alias at the solver phase, if the RHS expression is a particular type.

(None, ty_ref)

This means that we're missing information that we need in the bindings step:

  1. whether or not to generate the new type alias bindings
  2. whether or not we should process a string as a type or a string literal (we only call parse_type_literal on the ensure_type code path, so it only gets used in places where we're sure we're dealing with a type annotation - this means that stringified types don't work at all for implicit type aliases ATM, but they need to)

We could solve it by always generating the bindings, and having it resolve to either a type alias or some NotATypeAlias, and raise an error when we try to use the latter as a type alias. This feels pretty clunky though, and would be akin to having a bunch of Classes floating around where we need to verify that it's actually a class before we try to use it.

Activity

  1. changed the title [-]Recursive type aliases[/-] [+]Feature: Recursive type aliases[/+] on Apr 1, 2025
  2. added this to the V1 Release milestone on May 14, 2025
  3. self-assigned this
    on May 29, 2025
  4. github-actions commented on Jul 8, 2025

    @github-actions

    This issue has someone assigned, but has not had recent activity for more than 2 weeks.

    If you are still working on this issue, please add a comment so everyone knows. Otherwise, please unassign yourself and allow someone else to take over.

    Thank you for your contributions!

  5. yangdanny97 commented on Jul 24, 2025

    @yangdanny97
    CollaboratorAuthor

    One (possibly unreliable) heuristic for detecting implicit type aliases: | isn't supported between strings normally, so if we have an assignment whose RHS is a | node where at least one operand is a string literal & all the other operands are string literals or valid annotation syntax, we could try to handle it like a type alias.

    We'd need to make sure we don't panic if this fails.

  6. github-actions commented on Aug 12, 2025

    @github-actions

    This issue has someone assigned, but has not had recent activity for more than 2 weeks.

    If you are still working on this issue, please add a comment so everyone knows. Otherwise, please unassign yourself and allow someone else to take over.

    Thank you for your contributions!

  7. removed their assignment
    on Sep 22, 2025
  8. 36 remaining items

  9. JelleZijlstra commented on May 31, 2026

    @JelleZijlstra

    Pyrefly doesn't seem to support recursive aliases using the type statement:

    type R = list[tuple[int, R]]
    # ERROR sandbox.py:1:10-29: Found cyclic self-reference in `R` [[invalid-type-alias](https://pyrefly.org/en/docs/error-kinds/#invalid-type-alias)]`

    Other type checkers allow this.

  10. rchen152 commented on Jun 6, 2026

    @rchen152
    Contributor

    Ah, that's caused by Pyrefly's check for cyclic self-references misfiring (#3235). It's supposed to catch obviously silly things like type R = list[R], but it's too aggressive. If the inner R is unioned with something else (for example, type R = list[tuple[int, int | R]]), then pyrefly will accept it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Projects

No projects

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions