-
Notifications
You must be signed in to change notification settings - Fork 13.1k
Description
🔍 Search Terms
result monad
generic inference
type inference control flow
union type merging
generic unification
conditional branches return type
lost union type
err ok result inference
preserve generic branches
return type inference bug
typescript drops union type
async function result type inference
missing types
✅ Viability Checklist
- This wouldn't be a breaking change in existing TypeScript/JavaScript code
- This wouldn't change the runtime behavior of existing JavaScript code
- This could be implemented without emitting different JS based on the types of the expressions
- This isn't a runtime feature (e.g. library functionality, non-ECMAScript syntax with JavaScript output, new syntax sugar for JS, etc.)
- This isn't a request to add a new utility type: https://github.com/microsoft/TypeScript/wiki/No-New-Utility-Types
- This feature would agree with the rest of our Design Goals: https://github.com/Microsoft/TypeScript/wiki/TypeScript-Design-Goals
⭐ Suggestion
Hey folks 👋
I think I just ran into a really annoying inference hole, and I’m surely not the first one.
When returning multiple instances of a generic “Result” (or “Ok”/“Err” monad) from an async function, TypeScript drops some of the error types completely unless you explicitly annotate the return type.
I’m not asking TypeScript to infer full algebraic data types - just to preserve valid type information that already exists.
If all return branches are explicitly typed (for example:
Err<never, BadRequestError> and Err<never, ForbiddenError>),
the compiler should keep that as a discriminated union of both, rather than silently collapsing one of the generic parameters (like E) to never.
It currently feels like TypeScript “gives up” on merging generics with different instantiations, even when the outer generic constructor (e.g. Err<...>) is clearly the same.
Surely I’m not the first one annoyed by this - it’s a very common result-monad pattern.
Is there any ongoing discussion or design note about improving generic merging across conditional branches?
This seems like a pretty big ergonomic loss, especially compared to languages like Rust or Go, where return unions are explicit and reliable.
📃 Motivating Example
Here’s a minimal repro (no libraries, plain TS):
Typescrit playground link: CLICK
If you hover the inferred type of assignVerbose, you’ll see something like:
Promise<
| Ok<number, never>
| Err<never, BadRequestError>
| Err<never, ConfigError>
>
👉 The ForbiddenError is just gone from the union.
Even though it’s clearly one of the branches, TypeScript “squashes” it out because it can’t unify multiple instantiations of the same generic (Err<never, X>).
If I rewrite the same function in a chained or “functional” style (like neverthrow’s .andThen), everything works fine - the generic type parameter for E is preserved across the pipeline and inferred as:
Promise<Result<number, BadRequestError | ConfigError | ForbiddenError>>
So the difference isn’t semantic - it’s just that the compiler can’t reconcile Err<never, A> | Err<never, B> into Err<never, A|B> during inference.
💻 Use Cases
This basically means that any codebase using Result/Ok/Err patterns (neverthrow, fp-ts, custom monads, etc.) loses actual error type safety if it uses normal control flow instead of chaining.
TypeScript silently generalises the generic parameters to never instead of unioning them, which breaks the expected contract and can lead to unhandled runtime errors that TS can’t see.
This happens in many places:
- Service layers that validate business rules and return typed results.
- Functional-style APIs that rely on composable Result/Either flows.
- Async functions that use multiple return branches for Ok/Err values.
In larger codebases, it leads to:
- False confidence in exhaustiveness checks (result.error missing cases).
- Redundant type annotations everywhere just to force correct unions.
The type system can’t track all cases consistently, and well...throwing errors all around in 2025 feels like driving a car without seatbelts 🪩🚗💥