[ty] Add support for Literals in implicit type aliases (#21296)

## Summary

Add support for `Literal` types in implicit type aliases.

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

## Ecosystem analysis

This looks good to me, true positives and known problems.

## Test Plan

New Markdown tests.
This commit is contained in:
David Peter
2025-11-07 17:46:55 +01:00
committed by GitHub
parent 8ba1cfebed
commit ed18112cfa
6 changed files with 179 additions and 68 deletions

View File

@@ -181,30 +181,20 @@ def _(
bool2: Literal[Bool2],
multiple: Literal[SingleInt, SingleStr, SingleEnum],
):
# TODO should be `Literal[1]`
reveal_type(single_int) # revealed: @Todo(Inference of subscript on special form)
# TODO should be `Literal["foo"]`
reveal_type(single_str) # revealed: @Todo(Inference of subscript on special form)
# TODO should be `Literal[b"bar"]`
reveal_type(single_bytes) # revealed: @Todo(Inference of subscript on special form)
# TODO should be `Literal[True]`
reveal_type(single_bool) # revealed: @Todo(Inference of subscript on special form)
# TODO should be `None`
reveal_type(single_none) # revealed: @Todo(Inference of subscript on special form)
# TODO should be `Literal[E.A]`
reveal_type(single_enum) # revealed: @Todo(Inference of subscript on special form)
# TODO should be `Literal[1, "foo", b"bar", True, E.A] | None`
reveal_type(union_literals) # revealed: @Todo(Inference of subscript on special form)
reveal_type(single_int) # revealed: Literal[1]
reveal_type(single_str) # revealed: Literal["foo"]
reveal_type(single_bytes) # revealed: Literal[b"bar"]
reveal_type(single_bool) # revealed: Literal[True]
reveal_type(single_none) # revealed: None
reveal_type(single_enum) # revealed: Literal[E.A]
reveal_type(union_literals) # revealed: Literal[1, "foo", b"bar", True, E.A] | None
# Could also be `E`
reveal_type(an_enum1) # revealed: Unknown
# TODO should be `E`
reveal_type(an_enum2) # revealed: @Todo(Inference of subscript on special form)
reveal_type(an_enum2) # revealed: E
# Could also be `bool`
reveal_type(bool1) # revealed: Unknown
# TODO should be `bool`
reveal_type(bool2) # revealed: @Todo(Inference of subscript on special form)
# TODO should be `Literal[1, "foo", E.A]`
reveal_type(multiple) # revealed: @Todo(Inference of subscript on special form)
reveal_type(bool2) # revealed: bool
reveal_type(multiple) # revealed: Literal[1, "foo", E.A]
```
### Implicit type alias
@@ -246,28 +236,18 @@ def _(
bool2: Literal[Bool2],
multiple: Literal[SingleInt, SingleStr, SingleEnum],
):
# TODO should be `Literal[1]`
reveal_type(single_int) # revealed: @Todo(Inference of subscript on special form)
# TODO should be `Literal["foo"]`
reveal_type(single_str) # revealed: @Todo(Inference of subscript on special form)
# TODO should be `Literal[b"bar"]`
reveal_type(single_bytes) # revealed: @Todo(Inference of subscript on special form)
# TODO should be `Literal[True]`
reveal_type(single_bool) # revealed: @Todo(Inference of subscript on special form)
# TODO should be `None`
reveal_type(single_none) # revealed: @Todo(Inference of subscript on special form)
# TODO should be `Literal[E.A]`
reveal_type(single_enum) # revealed: @Todo(Inference of subscript on special form)
# TODO should be `Literal[1, "foo", b"bar", True, E.A] | None`
reveal_type(union_literals) # revealed: @Todo(Inference of subscript on special form)
reveal_type(single_int) # revealed: Literal[1]
reveal_type(single_str) # revealed: Literal["foo"]
reveal_type(single_bytes) # revealed: Literal[b"bar"]
reveal_type(single_bool) # revealed: Literal[True]
reveal_type(single_none) # revealed: None
reveal_type(single_enum) # revealed: Literal[E.A]
reveal_type(union_literals) # revealed: Literal[1, "foo", b"bar", True, E.A] | None
reveal_type(an_enum1) # revealed: Unknown
# TODO should be `E`
reveal_type(an_enum2) # revealed: @Todo(Inference of subscript on special form)
reveal_type(an_enum2) # revealed: E
reveal_type(bool1) # revealed: Unknown
# TODO should be `bool`
reveal_type(bool2) # revealed: @Todo(Inference of subscript on special form)
# TODO should be `Literal[1, "foo", E.A]`
reveal_type(multiple) # revealed: @Todo(Inference of subscript on special form)
reveal_type(bool2) # revealed: bool
reveal_type(multiple) # revealed: Literal[1, "foo", E.A]
```
## Shortening unions of literals

View File

@@ -33,7 +33,7 @@ g(None)
We also support unions in type aliases:
```py
from typing_extensions import Any, Never
from typing_extensions import Any, Never, Literal
from ty_extensions import Unknown
IntOrStr = int | str
@@ -54,6 +54,8 @@ NeverOrAny = Never | Any
AnyOrNever = Any | Never
UnknownOrInt = Unknown | int
IntOrUnknown = int | Unknown
StrOrZero = str | Literal[0]
ZeroOrStr = Literal[0] | str
reveal_type(IntOrStr) # revealed: types.UnionType
reveal_type(IntOrStrOrBytes1) # revealed: types.UnionType
@@ -73,6 +75,8 @@ reveal_type(NeverOrAny) # revealed: types.UnionType
reveal_type(AnyOrNever) # revealed: types.UnionType
reveal_type(UnknownOrInt) # revealed: types.UnionType
reveal_type(IntOrUnknown) # revealed: types.UnionType
reveal_type(StrOrZero) # revealed: types.UnionType
reveal_type(ZeroOrStr) # revealed: types.UnionType
def _(
int_or_str: IntOrStr,
@@ -93,6 +97,8 @@ def _(
any_or_never: AnyOrNever,
unknown_or_int: UnknownOrInt,
int_or_unknown: IntOrUnknown,
str_or_zero: StrOrZero,
zero_or_str: ZeroOrStr,
):
reveal_type(int_or_str) # revealed: int | str
reveal_type(int_or_str_or_bytes1) # revealed: int | str | bytes
@@ -112,6 +118,8 @@ def _(
reveal_type(any_or_never) # revealed: Any
reveal_type(unknown_or_int) # revealed: Unknown | int
reveal_type(int_or_unknown) # revealed: int | Unknown
reveal_type(str_or_zero) # revealed: str | Literal[0]
reveal_type(zero_or_str) # revealed: Literal[0] | str
```
If a type is unioned with itself in a value expression, the result is just that type. No
@@ -255,6 +263,68 @@ def _(list_or_tuple: ListOrTuple[int]):
reveal_type(list_or_tuple) # revealed: @Todo(Generic specialization of types.UnionType)
```
## `Literal`s
We also support `typing.Literal` in implicit type aliases.
```py
from typing import Literal
from enum import Enum
IntLiteral1 = Literal[26]
IntLiteral2 = Literal[0x1A]
IntLiterals = Literal[-1, 0, 1]
NestedLiteral = Literal[Literal[1]]
StringLiteral = Literal["a"]
BytesLiteral = Literal[b"b"]
BoolLiteral = Literal[True]
MixedLiterals = Literal[1, "a", True, None]
class Color(Enum):
RED = 0
GREEN = 1
BLUE = 2
EnumLiteral = Literal[Color.RED]
def _(
int_literal1: IntLiteral1,
int_literal2: IntLiteral2,
int_literals: IntLiterals,
nested_literal: NestedLiteral,
string_literal: StringLiteral,
bytes_literal: BytesLiteral,
bool_literal: BoolLiteral,
mixed_literals: MixedLiterals,
enum_literal: EnumLiteral,
):
reveal_type(int_literal1) # revealed: Literal[26]
reveal_type(int_literal2) # revealed: Literal[26]
reveal_type(int_literals) # revealed: Literal[-1, 0, 1]
reveal_type(nested_literal) # revealed: Literal[1]
reveal_type(string_literal) # revealed: Literal["a"]
reveal_type(bytes_literal) # revealed: Literal[b"b"]
reveal_type(bool_literal) # revealed: Literal[True]
reveal_type(mixed_literals) # revealed: Literal[1, "a", True] | None
reveal_type(enum_literal) # revealed: Literal[Color.RED]
```
We reject invalid uses:
```py
# error: [invalid-type-form] "Type arguments for `Literal` must be `None`, a literal value (int, bool, str, or bytes), or an enum member"
LiteralInt = Literal[int]
reveal_type(LiteralInt) # revealed: Unknown
def _(weird: LiteralInt):
reveal_type(weird) # revealed: Unknown
# error: [invalid-type-form] "`Literal[26]` is not a generic class"
def _(weird: IntLiteral1[int]):
reveal_type(weird) # revealed: Unknown
```
## Stringified annotations?
From the [typing spec on type aliases](https://typing.python.org/en/latest/spec/aliases.html):