Skip to content

feat(node): Filled in most util/types is* checks#540

Open
MendyBerger wants to merge 1 commit intounjs:mainfrom
MendyBerger:type-checks
Open

feat(node): Filled in most util/types is* checks#540
MendyBerger wants to merge 1 commit intounjs:mainfrom
MendyBerger:type-checks

Conversation

@MendyBerger
Copy link

@MendyBerger MendyBerger commented Mar 4, 2026

Not closing an issue as it's mostly just implementing a couple of functions that are notImplemented.

I also just did a general cleanup on the file and made the checks looks more alike.

While I was there I noticed a bug with the is[Primitive]Object implementations. They were checking with typeof, but typeof new String is actually "object" so I changed them to use instanceof.

Summary by CodeRabbit

  • Bug Fixes

    • Replaced many placeholder/no-op type checks with working implementations (promises, errors, functions, generators, iterators, boxed primitives, typed arrays, buffers, and more).
  • Refactor

    • Standardized utilities to accept unknown inputs and return precise type predicates with explicit runtime checks for safer, consistent behavior.
  • New Features

    • Added broader detection for Map/Set (generic-aware), array buffers, shared buffers, array-buffer views, typed-array variants, boxed primitives, and refined promise/type guards.

@MendyBerger MendyBerger requested a review from pi0 as a code owner March 4, 2026 17:12
@coderabbitai
Copy link

coderabbitai bot commented Mar 4, 2026

📝 Walkthrough

Walkthrough

This PR replaces many placeholder stubs in Node.js runtime type utilities with concrete, strongly-typed implementations using val: unknown and explicit runtime type-guards (instanceof, toString, constructor checks, ArrayBuffer checks, etc.).

Changes

Cohort / File(s) Summary
Type Guard Implementations
src/runtime/node/internal/util/types.ts
Replaced ~40+ placeholder stubs with concrete implementations. Standardized signatures to val: unknown and added precise TypeScript predicate return types. Implementations added for boxed primitives, errors, RegExp, Promise, async/generator functions/objects, Map/Set and iterators, ArrayBuffer family (including isAnyArrayBuffer, isArrayBufferView, typed arrays), and isBoxedPrimitive. Kept a few notImplemented exports (e.g., isProxy, isModuleNamespaceObject, isKeyObject, isExternal remains a false-returning guard).

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

🐇 I hopped through types with careful pace,
From boxed strings to buffers in their place,
instanceof and toString led the way,
Stubs became guards that now reliably stay,
A tidy burrow of checks — bright and brace.

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: implementing most of the util/types type-checking utilities that were previously stubs or incomplete.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/runtime/node/internal/util/types.ts`:
- Around line 103-105: The isSharedArrayBuffer type guard directly uses the
global SharedArrayBuffer which can be undefined in some runtimes; update the
guard (and the other direct SharedArrayBuffer references in this file) to first
check typeof SharedArrayBuffer !== 'undefined' before using instanceof so the
function returns false instead of throwing when SharedArrayBuffer is not
available (i.e., change isSharedArrayBuffer and the other checks to
short-circuit with a typeof guard).
- Around line 138-152: The isTypedArray implementation currently checks
instanceof Float16Array which can throw ReferenceError in runtimes without that
constructor; update the isTypedArray function to avoid direct constructor checks
and instead return ArrayBuffer.isView(val) &&
Object.prototype.toString.call(val) !== "[object DataView]". Locate the
isTypedArray export and replace the instanceof chain (including Float16Array)
with this safe detection logic so typed arrays are recognized across
environments that lack certain constructors.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 27ba8a0b-b434-4b29-af22-c5ff4cc729aa

📥 Commits

Reviewing files that changed from the base of the PR and between f89b7cc and 137b30c.

📒 Files selected for processing (1)
  • src/runtime/node/internal/util/types.ts

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/runtime/node/internal/util/types.ts`:
- Around line 44-52: The isAsyncFunction and isGeneratorFunction helpers are
vulnerable to constructor spoofing via checking val.constructor?.name; change
detection to use Object.prototype.toString.call(val) to compare against "[object
AsyncFunction]" and "[object GeneratorFunction]" (while still ensuring typeof
val === "function") so spoofed constructor objects can't produce false
positives; update the exported functions isAsyncFunction and isGeneratorFunction
to use this robust check while preserving their TypeScript typings.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 0b77b447-dd71-4413-9ac7-fb96f1dd7514

📥 Commits

Reviewing files that changed from the base of the PR and between 137b30c and cffb6c1.

📒 Files selected for processing (1)
  • src/runtime/node/internal/util/types.ts

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/runtime/node/internal/util/types.ts`:
- Around line 16-35: Replace the unsafe instanceof checks in isBigIntObject,
isBooleanObject, isNumberObject, isStringObject, isSymbolObject (and analogous
boxed-primitive checks later) with an internal-slot detection that calls the
primitive's valueOf to verify it's a true boxed primitive; e.g., for
isStringObject use a guarded call like try { return typeof
Object.prototype.valueOf.call(val) === 'string'; } catch { return false; }
(analogous checks: 'number', 'boolean', 'bigint', 'symbol') so prototype-forged
objects (Object.create(String.prototype)) are rejected; update isBoxedPrimitive
to use those new helpers.
- Around line 67-81: The isMap and isSet functions currently use instanceof
which misclassifies cross-realm objects and prototype fakes; replace those
checks with realm-safe internal tag checks using
Object.prototype.toString.call(val) === '[object Map]' for isMap and ===
'[object Set]' for isSet (keep the same exported names isMap and isSet and type
signatures), so cross-realm Map/Set values are accepted and objects merely
inheriting Map.prototype are rejected.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: a06c599e-ae29-43d1-98da-30fac3148f8f

📥 Commits

Reviewing files that changed from the base of the PR and between cffb6c1 and e8cd272.

📒 Files selected for processing (1)
  • src/runtime/node/internal/util/types.ts

Comment on lines +16 to 35
export const isBigIntObject: typeof nodeUtilTypes.isBigIntObject = (
val: unknown,
): val is BigInt => val instanceof BigInt;

export const isBooleanObject: typeof nodeUtilTypes.isBooleanObject = (
val,
): val is boolean => typeof val === "boolean";
val: unknown,
): val is Boolean => val instanceof Boolean;

export const isNumberObject: typeof nodeUtilTypes.isNumberObject = (
val,
): val is number => typeof val === "number";
val: unknown,
): val is Number => val instanceof Number;

export const isStringObject: typeof nodeUtilTypes.isStringObject = (
val,
): val is string => typeof val === "string";
val: unknown,
): val is String => val instanceof String;

export const isSymbolObject: typeof nodeUtilTypes.isSymbolObject = (
val,
): val is symbol => typeof val === "symbol";
val: unknown,
): val is Symbol => val instanceof Symbol;

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

cat -n src/runtime/node/internal/util/types.ts | head -40

Repository: unjs/unenv

Length of output: 1654


🏁 Script executed:

cat -n src/runtime/node/internal/util/types.ts | sed -n '125,140p'

Repository: unjs/unenv

Length of output: 703


🏁 Script executed:

node <<'NODE'
const { types } = require('node:util');

const fakeString = Object.create(String.prototype);
console.log('node util.types.isStringObject(fakeString):', types.isStringObject(fakeString));
console.log('fakeString instanceof String:', fakeString instanceof String);

const fakeNumber = Object.create(Number.prototype);
console.log('node util.types.isNumberObject(fakeNumber):', types.isNumberObject(fakeNumber));
console.log('fakeNumber instanceof Number:', fakeNumber instanceof Number);

const fakeBigInt = Object.create(BigInt.prototype);
console.log('node util.types.isBigIntObject(fakeBigInt):', types.isBigIntObject(fakeBigInt));
console.log('fakeBigInt instanceof BigInt:', fakeBigInt instanceof BigInt);

const fakeBoolean = Object.create(Boolean.prototype);
console.log('node util.types.isBooleanObject(fakeBoolean):', types.isBooleanObject(fakeBoolean));
console.log('fakeBoolean instanceof Boolean:', fakeBoolean instanceof Boolean);

const fakeSymbol = Object.create(Symbol.prototype);
console.log('node util.types.isSymbolObject(fakeSymbol):', types.isSymbolObject(fakeSymbol));
console.log('fakeSymbol instanceof Symbol:', fakeSymbol instanceof Symbol);
NODE

Repository: unjs/unenv

Length of output: 482


Replace instanceof checks with internal-slot detection for boxed primitives.

Lines 18, 22, 26, 30, 34 use instanceof which accepts prototype-forged objects (e.g., Object.create(String.prototype)), while Node's util.types correctly rejects them. This causes false positives in isBigIntObject, isBooleanObject, isNumberObject, isStringObject, isSymbolObject, and isBoxedPrimitive.

Detect internal slots by calling valueOf instead:

Suggested fix
+const hasBoxedSlot = (
+  val: unknown,
+  valueOf: (this: unknown) => unknown,
+): boolean => {
+  if (typeof val !== "object" || val === null) {
+    return false;
+  }
+  try {
+    valueOf.call(val);
+    return true;
+  } catch {
+    return false;
+  }
+};

 export const isBigIntObject: typeof nodeUtilTypes.isBigIntObject = (
   val: unknown,
-): val is BigInt => val instanceof BigInt;
+): val is BigInt => hasBoxedSlot(val, BigInt.prototype.valueOf);

 export const isBooleanObject: typeof nodeUtilTypes.isBooleanObject = (
   val: unknown,
-): val is Boolean => val instanceof Boolean;
+): val is Boolean => hasBoxedSlot(val, Boolean.prototype.valueOf);

 export const isNumberObject: typeof nodeUtilTypes.isNumberObject = (
   val: unknown,
-): val is Number => val instanceof Number;
+): val is Number => hasBoxedSlot(val, Number.prototype.valueOf);

 export const isStringObject: typeof nodeUtilTypes.isStringObject = (
   val: unknown,
-): val is String => val instanceof String;
+): val is String => hasBoxedSlot(val, String.prototype.valueOf);

 export const isSymbolObject: typeof nodeUtilTypes.isSymbolObject = (
   val: unknown,
-): val is Symbol => val instanceof Symbol;
+): val is Symbol => hasBoxedSlot(val, Symbol.prototype.valueOf);

 export const isBoxedPrimitive: typeof nodeUtilTypes.isBoxedPrimitive = (
   val: unknown,
 ): val is String | Number | Boolean | BigInt | Symbol => {
-  return val instanceof String ||
-    val instanceof Number ||
-    val instanceof BigInt ||
-    val instanceof Boolean ||
-    val instanceof Symbol;
+  return isStringObject(val) ||
+    isNumberObject(val) ||
+    isBigIntObject(val) ||
+    isBooleanObject(val) ||
+    isSymbolObject(val);
 };

Also applies to lines 128–136.

🧰 Tools
🪛 ESLint

[error] 18-18: Prefer using the primitive bigint as a type name, rather than the upper-cased BigInt.

(@typescript-eslint/no-wrapper-object-types)


[error] 18-18: Avoid using instanceof for type checking as it can lead to unreliable results.

(unicorn/no-instanceof-builtins)


[error] 22-22: Prefer using the primitive boolean as a type name, rather than the upper-cased Boolean.

(@typescript-eslint/no-wrapper-object-types)


[error] 22-22: Avoid using instanceof for type checking as it can lead to unreliable results.

(unicorn/no-instanceof-builtins)


[error] 26-26: Prefer using the primitive number as a type name, rather than the upper-cased Number.

(@typescript-eslint/no-wrapper-object-types)


[error] 26-26: Avoid using instanceof for type checking as it can lead to unreliable results.

(unicorn/no-instanceof-builtins)


[error] 30-30: Prefer using the primitive string as a type name, rather than the upper-cased String.

(@typescript-eslint/no-wrapper-object-types)


[error] 30-30: Avoid using instanceof for type checking as it can lead to unreliable results.

(unicorn/no-instanceof-builtins)


[error] 34-34: Prefer using the primitive symbol as a type name, rather than the upper-cased Symbol.

(@typescript-eslint/no-wrapper-object-types)


[error] 34-34: Avoid using instanceof for type checking as it can lead to unreliable results.

(unicorn/no-instanceof-builtins)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/runtime/node/internal/util/types.ts` around lines 16 - 35, Replace the
unsafe instanceof checks in isBigIntObject, isBooleanObject, isNumberObject,
isStringObject, isSymbolObject (and analogous boxed-primitive checks later) with
an internal-slot detection that calls the primitive's valueOf to verify it's a
true boxed primitive; e.g., for isStringObject use a guarded call like try {
return typeof Object.prototype.valueOf.call(val) === 'string'; } catch { return
false; } (analogous checks: 'number', 'boolean', 'bigint', 'symbol') so
prototype-forged objects (Object.create(String.prototype)) are rejected; update
isBoxedPrimitive to use those new helpers.

Comment on lines +67 to +81
export const isMap: typeof nodeUtilTypes.isMap = <T>(
val: unknown,
): val is T extends ReadonlyMap<any, any>
? (unknown extends T ? never : ReadonlyMap<any, any>)
: Map<unknown, unknown> => {
return val instanceof Map;
};

// @ts-ignore
export const isSet: typeof nodeUtilTypes.isSet = (val): val is Set =>
val instanceof Set;
export const isSet: typeof nodeUtilTypes.isSet = <T>(
val: unknown,
): val is T extends ReadonlySet<any>
? unknown extends T ? never : ReadonlySet<any>
: Set<unknown> => {
return val instanceof Set;
};
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

cat -n src/runtime/node/internal/util/types.ts | head -90

Repository: unjs/unenv

Length of output: 3597


🏁 Script executed:

node <<'NODE'
const vm = require('node:vm');
const { types } = require('node:util');

const crossRealmMap = vm.runInNewContext('new Map([["k",1]])');
console.log('node util.types.isMap(crossRealmMap):', types.isMap(crossRealmMap));
console.log('crossRealmMap instanceof Map:', crossRealmMap instanceof Map);

const fakeMap = Object.create(Map.prototype);
console.log('node util.types.isMap(fakeMap):', types.isMap(fakeMap));
console.log('fakeMap instanceof Map:', fakeMap instanceof Map);

const crossRealmSet = vm.runInNewContext('new Set([1])');
console.log('node util.types.isSet(crossRealmSet):', types.isSet(crossRealmSet));
console.log('crossRealmSet instanceof Set:', crossRealmSet instanceof Set);

const fakeSet = Object.create(Set.prototype);
console.log('node util.types.isSet(fakeSet):', types.isSet(fakeSet));
console.log('fakeSet instanceof Set:', fakeSet instanceof Set);
NODE

Repository: unjs/unenv

Length of output: 345


🌐 Web query:

Node.js util.types.isMap isSet implementation C++ internal type checking

💡 Result:

In Node.js, util.types.isMap() / util.types.isSet() are not implemented as JS instanceof checks. They’re exposed from a C++ internal binding that delegates to V8’s internal type predicates.

  • The JS glue is lib/internal/util/types.js, which exports (among other helpers) ...internalBinding('types')—i.e., it re-exports the C++ “types” binding directly. [1]

  • The C++ binding is registered in src/node_types.cc via NODE_BINDING_CONTEXT_AWARE_INTERNAL(types, node::InitializeTypes). [2]

  • In that file, Node defines functions like IsMap / IsSet that simply do:

    • args.GetReturnValue().Set(args[0]->IsMap());
    • args.GetReturnValue().Set(args[0]->IsSet());

    meaning the decision is made by V8’s v8::Value::IsMap() / IsSet() checks (and Node also wires “Fast API” versions via SetFastMethodNoSideEffect(...)). [2]

From the public API side, Node’s docs describe util.types.isMap(value) as returning true “if the value is a built-in Map instance” (same idea for isSet). [3]

Sources:
[1] lib/internal/util/types.js (Node.js) re-exporting internalBinding('types')
[2] src/node_types.cc (Node.js) implementing IsMap / IsSet via value->IsMap() / value->IsSet() and registering the binding
[3] Node.js util documentation for util.types.isMap()


Replace instanceof with realm-safe type checks to match Node.js behavior.

The current implementation at lines 72 and 80 using instanceof Map and instanceof Set fails to achieve Node parity:

  • False negative: cross-realm Maps/Sets (created in different contexts via vm.runInNewContext) are incorrectly rejected
  • False positive: fake objects created with Object.create(Map.prototype) are incorrectly accepted

Node.js's util.types.isMap() and util.types.isSet() use V8's internal type predicates which correctly handle both cases. The current implementation needs realm-safe checking to match this behavior.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/runtime/node/internal/util/types.ts` around lines 67 - 81, The isMap and
isSet functions currently use instanceof which misclassifies cross-realm objects
and prototype fakes; replace those checks with realm-safe internal tag checks
using Object.prototype.toString.call(val) === '[object Map]' for isMap and ===
'[object Set]' for isSet (keep the same exported names isMap and isSet and type
signatures), so cross-realm Map/Set values are accepted and objects merely
inheriting Map.prototype are rejected.

@MendyBerger
Copy link
Author

If there's any human looking at this, are we actually concerned about the increasingly far-fetched suggestions the llm keeps making here? I'm happy to do the changes it wants, but for each code change I accept, it finds an even deeper edge case.

I don't think the rest of this codebase has this level of scrutiny applied to it.

Thoughts?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant