Repository navigation
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) intypescript@7.1.0-beta,7.1.0-dev.20261011.1,7.1.0-dev.20261104.1, and7.1.0-dev.20261105.1. - With
typescript@6.0.3(ts.createSourceFile),'parameters' in nodeis 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!)); // TypeErrortsc (7.1.0-beta and 7.1.0-dev.20261105.1) reports no error for this file.
🙁 Actual behavior
'parameters' in identifierevaluates totrue, whileidentifier.parametersisundefined. Nodes returned by the 7.1 API areRemoteNodeinstances, and getters for all child-property names are defined onRemoteNode.prototype. Consequently, anincheck for any of these properties succeeds regardless of the node's kind. Examples include'name','statements', and'body'.parameterCount(identifier)passes type checking but throwsTypeError: Cannot read properties of undefined (reading 'length')at runtime. The checker narrowsnodetoArrowFunctionbased on'parameters' in node, even though the runtime check also succeeds for anIdentifier.hasOwnPropertydoes 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 nodeshould be true only for node kinds that have aparametersproperty, as in TypeScript 6.0's API. In particular,'parameters' in identifiershould befalse.
Alternatively, if using a shared RemoteNode prototype with getters for all child properties is intentional:
- Document explicitly that
inchecks on child properties cannot distinguish node kinds, and thathasOwnPropertyis 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 asisArrowFunctionorisSignatureDeclaration, 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 nodetotrueonly for theArrowFunction. The 7.1 API evaluates it totruefor 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 alwaysfalse, so the branch never executed. - Empty parameter lists are not the problem. Function-like nodes with a
parametersproperty have aNodeArray, whose length is0when the parameter list is empty. - Related: Replace Node interface with discriminated unions #56275 and Replace Node interface with discriminated unions #64040 discuss representing
Nodeas a discriminated union. With a union-typedNode, property-presence checks such as'x' in nodewould also participate in narrowing, making this runtime mismatch relevant to a wider range of AST code.
Metadata
Metadata
Assignees
Labels
No labels
Activity
MartinJohns commentedon Nov 6, 2026
Just pointing out that the
inoperator is intentionally unsound. Objects are not sealed and upcasting is implicitly allowed (assign type with the property to a type without it).k-taro56 commentedon Nov 6, 2026
Martin Johns (@MartinJohns)
this issue is specifically about how the runtime behavior has changed since 6.0. Thank you.