Answer typeof on builtin import bindings by the export's kind

`const { inspect } = require("util"); typeof inspect` printed "string", as did `import { inspect } from "node:util"` in an ES module and `typeof promises` from `fs`. JavaScript sources represent builtin values without their own lowering as identity tokens (strings), and typeof answered from that representation. typeof on a builtin import binding in a JavaScript source now answers "function" for exported functions and classes and "object" for exported objects; the binding read has no effects. TypeScript sources compile as before.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Malte Ubl
2026-10-18 08:32:58 -07:00
co-authored by Claude Opus 5.5
parent 95712ea590
commit 15096a47ed
5 changed files with 62 additions and 0 deletions
@@ -678,6 +678,30 @@ function lowerExprInner(lowerer: Lowerer, expr: ts.Expression): IrExpr {
if (!sym) {
return { kind: "strLit", value: "undefined", type: STRING, loc };
}
// `const { inspect } = require("util")` / `import { inspect } from
// "node:util"`: a binding of a builtin module's exported function,
// class or object. JavaScript sources represent such a value without
// its own lowering as an identity token (a string), so the operand's
// representation cannot answer. Node's answer follows the export's
// kind, and the binding read has no effects.
const imported = isJsSourceFile(tested.getSourceFile())
? lowerer.builtinImportOf(tested)
: null;
if (
imported &&
builtinModuleConstOf(lowerer, imported.module, imported.member) === undefined
) {
const t = lowerer.typeOf(tested);
if (
lowerer.checker.getCallSignatures(t).length > 0 ||
lowerer.checker.getConstructSignatures(t).length > 0
) {
return { kind: "strLit", value: "function", type: STRING, loc };
}
if ((t.flags & ts.TypeFlags.Object) !== 0) {
return { kind: "strLit", value: "object", type: STRING, loc };
}
}
if (lowerer.isStdlibSymbol(sym)) {
const t = lowerer.typeOf(expr.expression);
if (
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/6103-typeof-builtin-bindings.cjs"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/6104-typeof-builtin-bindings.mjs"
],
"diags": []
}
@@ -0,0 +1,15 @@
// typeof on bindings destructured from Node builtin modules: Node answers by
// the export's kind ("function" for functions and classes, "object" for
// namespace objects), never by how the binding is represented.
const { inspect, format, promisify, deprecate, isDeepStrictEqual } = require("util");
const { join, resolve } = require("path");
const { readFileSync, promises } = require("fs");
const { EventEmitter } = require("events");
const { createHash } = require("crypto");
const util = require("node:util");
const { inspect: aliased } = util;
console.log(typeof inspect, typeof format, typeof promisify, typeof deprecate, typeof isDeepStrictEqual);
console.log(typeof join, typeof resolve, typeof readFileSync, typeof promises);
console.log(typeof EventEmitter, typeof createHash, typeof aliased, typeof util.inspect);
console.log(typeof inspect === "function" ? "callable" : "not callable");
console.log(join("a", "b"), format("%s=%d", "x", 1), inspect({ a: [1, 2] }));
@@ -0,0 +1,11 @@
// typeof on named, default-destructured and namespace-destructured builtin
// bindings in an ES module: Node answers by the export's kind.
import util, { inspect as namedInspect, format } from "node:util";
import * as path from "node:path";
import { createHash } from "node:crypto";
import { promises } from "node:fs";
const { inspect, promisify } = util;
const { join } = path;
console.log(typeof inspect, typeof promisify, typeof namedInspect, typeof format);
console.log(typeof join, typeof createHash, typeof util.inspect, typeof promises);
console.log(namedInspect("s"), format("%d", 2), join("x", "y"));