Type inference issue with discriminated unions in .otherwise() - remaining types not properly narrowed
enhancement
## Description
When pattern matching on discriminated unions with specific literal types, the .otherwise() clause should only include the remaining unmatched cases. However, `ts-pattern` currently shows the parameter type as the full original union instead of just the remaining unmatched types.
## Reproduction
```ts
import { match } from "ts-pattern";
// Simple discriminated union
type Animal =
| { type: "dog"; breed: string }
| { type: "cat"; color: string }
| { type: "bird"; wingspan: number };
// Problem case
let animal: Animal;
match(animal)
.with({ type: "dog" }, ({ breed }) => {
// This correctly matches { type: "dog"; breed: string }
console.log(`Dog breed: ${breed}`);
})
.with({ type: "cat" }, ({ color }) => {
// This correctly matches { type: "cat"; color: string }
console.log(`Cat color: ${color}`);
})
.otherwise((remaining) => {
// ISSUE: `remaining` should be typed as { type: "bird"; wingspan: number }
// But it's currently typed as the full Animal union
// TypeScript should know that only "bird" is possible here
console.log(remaining.type); // Should know this can only be "bird"
// remaining.wingspan should be accessible without type assertion
});
```
## Expected Behavior
The parameter in .otherwise() should be typed as { type: "bird"; wingspan: number } since:
1. { type: "dog" } cases are handled by the first pattern
2. { type: "cat" } cases are handled by the second pattern
3. Only { type: "bird" } cases remain unmatched
## Actual Behavior
The parameter in .otherwise() is typed as the full Animal union type, which includes all variants even though some have already been matched.
### Even Simpler Example
```ts
type Status = "loading" | "success" | "error";
let status: Status;
match(status)
.with("loading", () => console.log("Loading..."))
.with("success", () => console.log("Success!"))
.otherwise((remaining) => {
// ISSUE: `remaining` should be typed as "error"
// But it's currently typed as "loading" | "success" | "error"
console.log(remaining); // Should know this can only be "error"
});
```
## Analysis
This appears to be a limitation in how `ts-pattern` computes the "remaining" or "unmatched" types for exhaustiveness checking. The library should be able to:
- Track which specific union members have been explicitly matched
- Compute the remaining unmatched union members
- Apply this narrowing to the .otherwise() parameter type
This is particularly important for discriminated unions where precise type narrowing enables better type safety and developer experience.
### Solution
Use the same trick as `exhaustive()`
```
otherwise<c>(
handler: (value: DeepExcludeAll<i, handledCases>) => PickReturnValue<o, c>
): PickReturnValue<o, Union<inferredOutput, c>>;
```
## Already open thread
- https://github.com/gvergnaud/ts-pattern/issues/235
关闭于 2025-07-27 4 条评论