My primary reaction seems similar to Mark Shannon's.
When I see this code:
def is_str_list(val: List[object]) -> TypeGuard[List[str]]:
...
I cannot tell what it returns. There is no readable indication in that this returns a boolean so the reader cannot immediately see how to use the function. In fact my first reaction is to assume it returns some form of List[str]. Which it most definitely does not do.
Additionally, it seems like restricting this concept of a type guard to only a function that returns a bool is wrong.
It is also a natural idiom to encounter functions that raise an exception if their type conditions aren't met.
Those should also narrow types within a non-exception caught codepath after their return. Be simple and assume that catching any exception from their call ends their narrowing?
A narrowing is meta-information about a typing side effect, it isn't a type itself. It isn't a value. So replacing a return type with it seems like too much. But if we do wind up going that route, at least make the name indicate that the return value is a bool. i.e.: def parse_num(thing: Any) -> NarrowingBool[float]
Perhaps this would be better expressed as an annotation on the argument(s) that the function narrows?
def is_str_list(val: Constrains[List[object]:List[str]) -> bool:
...
I'm using Constrains as an alternative to Narrows here... still open to suggestions. But I do not think we should get hung up on TypeScript and assume that TypeGuard is somehow a good name. This particular proposed example uses : slice notation rather than a comma as a proposal that may be more readable. I'd also consider using the 'as' keyword instead of , or : but that wouldn't even parse today. Really what might be nice for this is our -> arrow.
def assert_str_list(var: Narrows[List[object] -> List[str]]) -> None:
...
as the arrow carries the implication of something effective "after" the call. I suspect adding new parsing syntax isn't what anyone had in mind for now though. : seems like a viable stopgap, whether syntax happens or not. (i'm intentionally avoiding the comma as an exploration in readability)
The key point I'm trying to represent is that declaring it per argument allows for more expressive functions and doesn't interfere with their return value (if they even return anything). Clearly there's a use case that _does_ want to make the narrowing conditional on the return value boolness. I still suggest expressing that on the arguments themselves. More specific flavors of the guarding parameter such as ConstrainsWhenTrue or NarrowsWhenFalse perhaps.
def is_set_of(val: Constrains[Set[Any]:Set[_T]], type: Type[_T]) -> bool:
...
which might also be written more concisely as:
def is_set_of(val: Set[Constrains[Any:_T]], type: Type[_T]) -> bool:
If we allow the full concept.
hopefully food for thought, i'm -1 right now.
-gps