Skip to content

TypeScript 7.1: in checks always succeed for child properties on RemoteNode, breaking type narrowing #65357

Open

Description

🔎 Search Terms

in operator, RemoteNode, typescript/ast, typescript/sync, property presence, narrowing, parameters, isSignatureDeclaration

🕗 Version & Regression Information

  • This is a change from TypeScript 6.0's API.
  • Reproduced with the 7.1 API (typescript/sync, typescript/ast) in typescript@7.1.0-beta, 7.1.0-dev.20261011.1, 7.1.0-dev.20261104.1, and 7.1.0-dev.20261105.1.
  • With typescript@6.0.3 (ts.createSourceFile), 'parameters' in node is true only for nodes of kinds that have parameters.

⏯ Playground Link

None (the API runs in Node.js).

💻 Code

// repro.mts
// npm i typescript@7.1.0-beta
// node repro.mts (Node.js strips the types)
import { API } from 'typescript/sync';
import { type ArrowFunction, type Identifier, isIdentifier, type Node } from 'typescript/ast';

function parameterCount(node: Identifier | ArrowFunction): number {
  if ('parameters' in node) {
    return node.parameters.length; // narrowed to ArrowFunction
  }
  return 0;
}

using api = new API({ cwd: process.cwd() });
using file = api.createSourceFile('/a.ts', 'const value = 1;');
let name: Identifier | undefined;
const visit = (node: Node): void => {
  if (isIdentifier(node)) name ??= node;
  node.forEachChild(visit);
};
visit(file.sourceFile);
console.log('parameters' in name!); // true
console.log(parameterCount(name!)); // TypeError

tsc (7.1.0-beta and 7.1.0-dev.20261105.1) reports no error for this file.

🙁 Actual behavior

  • 'parameters' in identifier evaluates to true, while identifier.parameters is undefined. Nodes returned by the 7.1 API are RemoteNode instances, and getters for all child-property names are defined on RemoteNode.prototype. Consequently, an in check for any of these properties succeeds regardless of the node's kind. Examples include 'name', 'statements', and 'body'.
  • parameterCount(identifier) passes type checking but throws TypeError: Cannot read properties of undefined (reading 'length') at runtime. The checker narrows node to ArrowFunction based on 'parameters' in node, even though the runtime check also succeeds for an Identifier.
  • hasOwnProperty does not provide an equivalent kind check: the shared getters are inherited from the prototype, not own properties of the individual nodes.

🙂 Expected behavior

Ideally, property-presence checks should remain consistent with the node interfaces used for type narrowing:

  • 'parameters' in node should be true only for node kinds that have a parameters property, as in TypeScript 6.0's API. In particular, 'parameters' in identifier should be false.

Alternatively, if using a shared RemoteNode prototype with getters for all child properties is intentional:

  • Document explicitly that in checks on child properties cannot distinguish node kinds, and that hasOwnProperty is not a substitute for kind checks.
  • Document this as a breaking change from the TypeScript 6.0 API, and recommend the kind guards already exported by typescript/ast, such as isArrowFunction or isSignatureDeclaration, where applicable.

Documentation would help users avoid the issue, although it would not by itself eliminate the mismatch between static narrowing and runtime behavior.

Additional information about the issue

  • For const value = 1; const f = () => value;, TypeScript 6.0.3 evaluates 'parameters' in node to true only for the ArrowFunction. The 7.1 API evaluates it to true for all 14 nodes.
  • This caused an actual failure in a compiler built on the 7.1 API. The check !('statements' in arrow.body), intended to select arrow functions with expression bodies, was always false, so the branch never executed.
  • Empty parameter lists are not the problem. Function-like nodes with a parameters property have a NodeArray, whose length is 0 when the parameter list is empty.
  • Related: Replace Node interface with discriminated unions #56275 and Replace Node interface with discriminated unions  #64040 discuss representing Node as a discriminated union. With a union-typed Node, property-presence checks such as 'x' in node would also participate in narrowing, making this runtime mismatch relevant to a wider range of AST code.

Activity

  1. MartinJohns commented on Nov 6, 2026

    @MartinJohns
    Contributor

    Just pointing out that the in operator is intentionally unsound. Objects are not sealed and upcasting is implicitly allowed (assign type with the property to a type without it).

  2. k-taro56 commented on Nov 6, 2026

    @k-taro56
    Author

    Martin Johns (@MartinJohns)
    this issue is specifically about how the runtime behavior has changed since 6.0. Thank you.

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

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions