Repository: TypeScript
Issue: Allow custom guards to return non-boolean values, with conditional type results
Labels: Suggestion,Awaiting More Feedback

# Suggestion

Sometimes we want to write a guard function that doesn't return a boolean. For example, 

```tsx
type Success<T> = { ok: true, value: T }
type Fail = { ok: false, error: string }
type Result<T> = Success<T> | Fail

function showError<T>(result: Result<T>): string | null {
  if (result.ok) {
    return null;
  }
  return `Oops, something went wrong: ${result.error}`;
}
```

here, we render a `Result<T>` value into either a `string` error message, or return `null` if there's no error. This kind of pattern can be used in React, for example, where we could write

```tsx
function Component() {
  const result: Result<number> = getResult();
  return <>{showError(result) ?? process(result.value)}</>;
}
```

However, if we try this code today, we get an error:

```Property 'value' does not exist on type 'Result<number>'.```

This makes sense, since tsc has no way of knowing that we're narrowing the value using `showError`.

Now, we can _almost_ reach for a custom type guard. We can easily write:

```tsx
function isError<T>(result: Result<T>): result is Fail {
  return !result.ok;
}
```

and using this function, we could write

```tsx
function Component() {
  const result: Result<number> = getResult();
  return <>{isError(result) ? showError(result) : process(result.value)}</>;
}
```

because tsc now uses the `isError` custom type guard to narrow the type of `result`.

However, this means that we can't use `showError` to narrow our value. Especially if the arguments to `isError`/`showError` are larger or more complicated, this means we have a lot of data that needs to be kept in-sync in source, or we'll render the wrong error (we need to ensure that whenever we `showError` a value, it's the _same_ as the value passed to `isError`).

Since our `showError` already provides enough context that narrowing is possible, ideally we'd be able to use it as-is by giving the compiler a hint about what its return types mean.

But we're limited by the fact that custom type guards must return `boolean` values.

---

The suggestion is to allow *custom type guard conditions*, such as the following:

```tsx
function showError<T>(result: Result<T>): (result is Fail) ? string : null {
  if (result.ok) {
    return null;
  }
  return `Oops, something went wrong: ${result.error}`;
}
```

Here, the return type is now annotated as `(result is Fail) ? string : null`. This means that if `result` is `Fail`, then the function returns a `string`, otherwise, it returns a `null`.

So if we verify that `showError(result)` is `null`, then that means that `result` must not have been a `Fail` value, so we can narrow it to a `Success<T>`. This means that `showError(result) ?? process(result.value)` will now pass, since on the right hand side of `??` we are able to narrow `showError(result)` to `null`, which means that we can narrow `result` to `Success<number>` which has a `.value` field.


<!--
  Please fill in each section completely. Thank you!
-->

## 🔍 Search Terms

- custom type guard
- conditional type guard

We can sort of see this as a way to explicitly annotate (but not check) control-flow for functions as described in #33912 in much the way that custom type guards are _used_ by tsc to narrow types, but the implementation is not really checked (other than that it returns a `boolean`).

## ✅ Viability Checklist



* [x] This wouldn't be a breaking change in existing TypeScript/JavaScript code
  * This new syntax doesn't overlap with existing syntax, so this will have no affect on existing code. Avoiding ambiguity with regular conditional types, or custom type guards that narrow to conditional types is important, but the proposed syntax avoids any conflicts
* [x] This wouldn't change the runtime behavior of existing JavaScript code
* [x] This could be implemented without emitting different JS based on the types of the expressions
* [x] This isn't a runtime feature (e.g. library functionality, non-ECMAScript syntax with JavaScript output, new syntax sugar for JS, etc.)
* [x] This feature would agree with the rest of [TypeScript's Design Goals](https://github.com/Microsoft/TypeScript/wiki/TypeScript-Design-Goals).


## ⭐ Suggestion

Custom type guard functions should optionally support conditional annotations in their return types, allowing them to return non-`boolean` types. A function can be annotated as:

```tsx
function myCustomTypeGuard(x: ArgType): (x is SmallerArgType) ? Positive : Negative {
  ...
}
```

If the result of the function call `myCustomTypeGuard(x)` is ever narrowed so that the result doesn't overlap at all with `Negative`, then `x` is automatically narrowed to `SmallerArgType`.

If the result of the function call `myCustomTypeGuard(x)` is ever narrowed so that the result doesn't overlap at all with `Positive`, then `x` is automatically narrowed to _exclude_ `SmallerArgType`.

The function's implementation is just checked to ensure that it returns `Positive | Negative`; just as with existing custom type guards, tsc ignores the actual logic used to decide whether the positive or negative value should be returned.


Essentially, the existing custom type guard function syntax `(arg: T): arg is Blah` is identical to the expanded form `(arg: T): (arg is Blah) ? true : false`.


## 📃 Motivating Example

```
import * as React from "react";

type Result<T> = { ok: true; value: T } | { ok: false; error: string };

// New syntax is here: conditional custom type guard annotation
function showError<T>(result: Result<T>): (result is Fail) ? string : null {
  if (result.ok) {
    return null;
  }
  return `Oops, something went wrong: ${result.error}`;
}

function getResult(): Result<number> {
  return null as any;
}
function process(x: number): string {
  return `the value is ${x}`;
}

function Component() {
  const result: Result<number> = getResult();
  return <>{showError(result) ?? process(result.value)}</>;
}
```

## 💻 Use Cases

This makes it easier to write flexible type-guards, such as the example above, especially for dealing with errors or processing "exceptional" cases.

It's possible to separately use a regular custom type guard function and then use a different function to process the result, but this can be redundant or error-prone, since you have to ensure that the two calls remain in sync with each other.