Skip to content
174 changes: 120 additions & 54 deletions crates/ty/docs/rules.md

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ def f(*args: Unpack[Ts]) -> tuple[Unpack[Ts]]:
reveal_type(Alias) # revealed: @Todo(Support for `typing.TypeAlias`)

def g() -> TypeGuard[int]: ...
def h() -> TypeIs[int]: ...
def i(callback: Callable[Concatenate[int, P], R_co], *args: P.args, **kwargs: P.kwargs) -> R_co:
reveal_type(args) # revealed: tuple[@Todo(Support for `typing.ParamSpec`), ...]
reveal_type(kwargs) # revealed: dict[str, @Todo(Support for `typing.ParamSpec`)]
Expand Down
264 changes: 264 additions & 0 deletions crates/ty_python_semantic/resources/mdtest/narrow/type_guards.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,264 @@
# User-defined type guards

User-defined type guards are functions of which the return type is either `TypeGuard[...]` or
`TypeIs[...]`.

## Display

```py
from ty_extensions import Intersection, Not, TypeOf
from typing_extensions import TypeGuard, TypeIs

def _(
a: TypeGuard[str],
b: TypeIs[str | int],
c: TypeGuard[Intersection[complex, Not[int], Not[float]]],
d: TypeIs[tuple[TypeOf[bytes]]],
e: TypeGuard, # error: [invalid-type-form]
f: TypeIs, # error: [invalid-type-form]
):
# TODO: Should be `TypeGuard[str]`
reveal_type(a) # revealed: @Todo(`TypeGuard[]` special form)
reveal_type(b) # revealed: TypeIs[str | int]
# TODO: Should be `TypeGuard[complex & ~int & ~float]`
reveal_type(c) # revealed: @Todo(`TypeGuard[]` special form)
reveal_type(d) # revealed: TypeIs[tuple[<class 'bytes'>]]
reveal_type(e) # revealed: Unknown
reveal_type(f) # revealed: Unknown

# TODO: error: [invalid-return-type] "Function always implicitly returns `None`, which is not assignable to return type `TypeGuard[str]`"
def _(a) -> TypeGuard[str]: ...

# error: [invalid-return-type] "Function always implicitly returns `None`, which is not assignable to return type `TypeIs[str]`"
def _(a) -> TypeIs[str]: ...
def f(a) -> TypeGuard[str]:
return True

def g(a) -> TypeIs[str]:
return True

def _(a: object):
# TODO: Should be `TypeGuard[a, str]`
reveal_type(f(a)) # revealed: @Todo(`TypeGuard[]` special form)
reveal_type(g(a)) # revealed: TypeIs[a, str]
```

## Parameters

A user-defined type guard must accept at least one positional argument (in addition to `self`/`cls`
for non-static methods).

```pyi
from typing_extensions import TypeGuard, TypeIs

# TODO: error: [invalid-type-guard-definition]
def _() -> TypeGuard[str]: ...

# TODO: error: [invalid-type-guard-definition]
def _(**kwargs) -> TypeIs[str]: ...

class _:
# fine
def _(self, /, a) -> TypeGuard[str]: ...
@classmethod
def _(cls, a) -> TypeGuard[str]: ...
@staticmethod
def _(a) -> TypeIs[str]: ...

# errors
def _(self) -> TypeGuard[str]: ... # TODO: error: [invalid-type-guard-definition]
def _(self, /, *, a) -> TypeGuard[str]: ... # TODO: error: [invalid-type-guard-definition]
@classmethod
def _(cls) -> TypeIs[str]: ... # TODO: error: [invalid-type-guard-definition]
@classmethod
def _() -> TypeIs[str]: ... # TODO: error: [invalid-type-guard-definition]
@staticmethod
def _(*, a) -> TypeGuard[str]: ... # TODO: error: [invalid-type-guard-definition]
```

For `TypeIs` functions, the narrowed type must be assignable to the declared type of that parameter,
if any.

```pyi
from typing import Any
from typing_extensions import TypeIs

def _(a: object) -> TypeIs[str]: ...
def _(a: Any) -> TypeIs[str]: ...
def _(a: tuple[object]) -> TypeIs[tuple[str]]: ...
def _(a: str | Any) -> TypeIs[str]: ...
def _(a) -> TypeIs[str]: ...

# TODO: error: [invalid-type-guard-definition]
def _(a: int) -> TypeIs[str]: ...

# TODO: error: [invalid-type-guard-definition]
def _(a: bool | str) -> TypeIs[int]: ...
```

## Arguments to special forms

`TypeGuard` and `TypeIs` accept exactly one type argument.

```py
from typing_extensions import TypeGuard, TypeIs

a = 123

# TODO: error: [invalid-type-form]
def f(_) -> TypeGuard[int, str]: ...

# error: [invalid-type-form]
def g(_) -> TypeIs[a, str]: ...

# TODO: Should be `Unknown`
reveal_type(f(0)) # revealed: @Todo(`TypeGuard[]` special form)
reveal_type(g(0)) # revealed: Unknown
```

## Return types

All code paths in a type guard function must return booleans.

```py
from typing_extensions import Literal, TypeGuard, TypeIs, assert_never

def _(a: object, flag: bool) -> TypeGuard[str]:
if flag:
return 0

return "foo"

# error: [invalid-return-type] "Function can implicitly return `None`, which is not assignable to return type `TypeIs[str]`"
def f(a: object, flag: bool) -> TypeIs[str]:
if flag:
# error: [invalid-return-type] "Return type does not match returned value: expected `TypeIs[str]`, found `float`"
return 1.2

def g(a: Literal["foo", "bar"]) -> TypeIs[Literal["foo"]]:
if a == "foo":
# Logically wrong, but allowed regardless
return False

return False
```

## Invalid calls

```py
from typing import Any
from typing_extensions import TypeGuard, TypeIs

def f(a: object) -> TypeGuard[str]:
return True

def g(a: object) -> TypeIs[int]:
return True

def _(d: Any):
if f(): # error: [missing-argument]
...

# TODO: Is this error correct?
if g(*d): # error: [missing-argument]
...

if f("foo"): # TODO: error: [invalid-type-guard-call]
...

if g(a=d): # error: [invalid-type-guard-call]
...

def _(a: tuple[str, int] | tuple[int, str]):
if g(a[0]):
# TODO: Should be `tuple[str, int]`
reveal_type(a) # revealed: tuple[str, int] | tuple[int, str]
```

## Narrowing

```py
from typing import Any
from typing_extensions import TypeGuard, TypeIs

def guard_str(a: object) -> TypeGuard[str]:
return True

def is_int(a: object) -> TypeIs[int]:
return True

def _(a: str | int):
if guard_str(a):
# TODO: Should be `str`
reveal_type(a) # revealed: str | int
else:
reveal_type(a) # revealed: str | int

if is_int(a):
reveal_type(a) # revealed: int
else:
reveal_type(a) # revealed: str & ~int

def _(a: str | int):
b = guard_str(a)
c = is_int(a)

reveal_type(a) # revealed: str | int
# TODO: Should be `TypeGuard[a, str]`
reveal_type(b) # revealed: @Todo(`TypeGuard[]` special form)
reveal_type(c) # revealed: TypeIs[a, int]

if b:
# TODO: Should be `str`
reveal_type(a) # revealed: str | int
else:
reveal_type(a) # revealed: str | int

if c:
reveal_type(a) # revealed: int
else:
reveal_type(a) # revealed: str & ~int

def _(x: str | int, flag: bool) -> None:
b = is_int(x)
reveal_type(b) # revealed: TypeIs[x, int]

if flag:
x = ""

if b:
# TODO: Should be `str | int`
reveal_type(x) # revealed: int
```

## `TypeGuard` special cases

```py
from typing import Any
from typing_extensions import TypeGuard, TypeIs

def guard_int(a: object) -> TypeGuard[int]:
return True

def is_int(a: object) -> TypeIs[int]:
return True

def does_not_narrow_in_negative_case(a: str | int):
if not guard_int(a):
# TODO: Should be `str`
reveal_type(a) # revealed: str | int
else:
reveal_type(a) # revealed: str | int

def narrowed_type_must_be_exact(a: object, b: bool):
if guard_int(b):
# TODO: Should be `int`
reveal_type(b) # revealed: bool

if isinstance(a, bool) and is_int(a):
reveal_type(a) # revealed: bool

if isinstance(a, bool) and guard_int(a):
# TODO: Should be `int`
reveal_type(a) # revealed: bool
```
Original file line number Diff line number Diff line change
Expand Up @@ -789,4 +789,17 @@ def g3(obj: Foo[tuple[A]]):
f3(obj)
```

## `TypeGuard` and `TypeIs`

`TypeGuard[...]` and `TypeIs[...]` are always assignable to `bool`.

```py
from ty_extensions import Unknown, is_assignable_to, static_assert
from typing_extensions import Any, TypeGuard, TypeIs

# TODO: TypeGuard
# static_assert(is_assignable_to(TypeGuard[Unknown], bool))
static_assert(is_assignable_to(TypeIs[Any], bool))
```

[typing documentation]: https://typing.python.org/en/latest/spec/concepts.html#the-assignable-to-or-consistent-subtyping-relation
Original file line number Diff line number Diff line change
Expand Up @@ -402,6 +402,17 @@ static_assert(is_disjoint_from(TypeOf[C.prop], D))
static_assert(is_disjoint_from(D, TypeOf[C.prop]))
```

### `TypeGuard` and `TypeIs`

```py
from ty_extensions import static_assert, is_disjoint_from
from typing_extensions import TypeIs

# TODO: TypeGuard
# static_assert(not is_disjoint_from(bool, TypeGuard[str]))
static_assert(not is_disjoint_from(bool, TypeIs[str]))
```

## Callables

No two callable types are disjoint because there exists a non-empty callable type
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -342,6 +342,35 @@ static_assert(is_subtype_of(Intersection[LiteralString, Not[Literal[""]]], Not[A
static_assert(is_subtype_of(Intersection[LiteralString, Not[Literal["", "a"]]], Not[AlwaysFalsy]))
```

### `TypeGuard` and `TypeIs`

`TypeGuard[...]` and `TypeIs[...]` are subtypes of `bool`.

```py
from ty_extensions import is_subtype_of, static_assert
from typing_extensions import TypeGuard, TypeIs

# TODO: TypeGuard
# static_assert(is_subtype_of(TypeGuard[int], bool))
static_assert(is_subtype_of(TypeIs[str], bool))
```

`TypeIs` is invariant. `TypeGuard` is covariant.

```py
from ty_extensions import is_equivalent_to, is_subtype_of, static_assert
from typing_extensions import TypeGuard, TypeIs

# TODO: TypeGuard
# static_assert(is_subtype_of(TypeGuard[int], TypeGuard[int]))
# static_assert(is_subtype_of(TypeGuard[bool], TypeGuard[int]))
static_assert(is_subtype_of(TypeIs[int], TypeIs[int]))

static_assert(not is_subtype_of(TypeGuard[int], TypeGuard[bool]))
static_assert(not is_subtype_of(TypeIs[bool], TypeIs[int]))
static_assert(not is_subtype_of(TypeIs[int], TypeIs[bool]))
```

### Module literals

```py
Expand Down
Loading
Loading