镜像站点 · 本页由第三方 GitHub 只读镜像提供,非 GitHub 官方站点,不接受任何登录或凭据输入。前往 github.com
Skip to content

Enhance Control‑Flow Narrowing for never‑returning Functions Passed as Parameters #62235

Closed as not planned

Description

Rationale

TypeScript currently narrows types when a function known at compile time (e.g., a throwError() declared with return type never) is invoked. However, when the thrower function is injected via a parameter - even if typed to return never - TS does not narrow, because it cannot assume that caller will always pass in a never function. This limitation forces developers into workarounds and undermines ergonomics for generic code patterns.

Code

function throwError(message: string): never {
  throw new Error(message);
}

function creatorFunction(fn: (data: unknown, error: typeof throwError) => Promise<any>) {
  return async (data: unknown) => await fn(data, throwError);
}

const getUserName = creatorFunction(async (_, throwErrorFromInside) => {
  // Simulating a user fetch operation
  const user = Math.random() < 0.5 ? null : { name: "test" };

  if (!user) throwErrorFromInside("User not found");
  console.log(user.name); // ❌ TypeScript error: 'user' is possibly 'null'

  if (!user) throwError("User not found");
  console.log(user.name); // ✅ No error here
});

Actual behavior

TypeScript does not narrow the user variable after throwErrorFromInside(...) is called, even though:

  • The parameter is typed as a never-returning function.
  • The control flow after the call should be unreachable if the condition is true.
  • The exact same code works when calling the throwError(...) function directly.

Expected behavior

TypeScript should treat:

if (!user) throwErrorFromInside(...);

as equivalent to:

if (!user) throwError(...);

and narrow user accordingly — i.e., allow user.name with no error afterward.

Technical Considerations / Trade-offs

  • No changes to emitted JS; strictly compile-time.
  • Minimal and safe extension to control-flow analysis.
  • Could reuse existing mechanisms for recognizing class/global functions, but would need enhancements to support parameter references where the function is known to return never.

Backward Compatibility

No breaking changes; only narrowing behavior is enhanced for specific patterns.

Conclusion

The ask is modest but meaningful: allow TypeScript to narrow after calls to never-typed function parameters when safe. This eliminates common developer frustrations without runtime impact.

Activity

  1. MartinJohns commented on Aug 8, 2025

    @MartinJohns
    Contributor

    This is intentional and for performance reasons.

  2. mattiamalonni commented on Aug 8, 2025

    @mattiamalonni
    Author

    Maybe, but currently there is no work around. Suppose throwErrorFromInside needs to be generic and inherit types from the creatorFunction. How can i achieve this?

  3. MartinJohns commented on Aug 8, 2025

    @MartinJohns
    Contributor

    You can find the requirements here: #32695

    A function call is analyzed as an assertion call or never-returning call when

    • the call occurs as a top-level expression statement, and
    • the call specifies a single identifier or a dotted sequence of identifiers for the function name, and
    • each identifier in the function name references an entity with an explicit type, and
    • the function name resolves to a function type with an asserts return type or an explicit never return type annotation.

    Your case works if you explicitly type throwErrorFromInside: throwErrorFromInside: (_: string) => never

  4. mattiamalonni commented on Aug 8, 2025

    @mattiamalonni
    Author

    Right. Thats a good point.

    2 more considerations:

    • Why we should cast the type every time? Can't TypeScript get it by itself?
    • What about generic type inheritance. In this way, we must be cast them every time.
  5. typescript-bot commented on Aug 11, 2025

    @typescript-bot
    Contributor

    This issue has been marked as "Duplicate" and has seen no recent activity. It has been automatically closed for house-keeping purposes.

  6. locked as resolved and limited conversation to collaborators on Oct 22, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    DuplicateAn existing issue was already created

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions