How to fix Cannot narrow type with instanceof for built-in types in TypeScript

TypeScriptINTERMEDIATEMEDIUMUpdated Dec 10, 2025· 93 views

This TypeScript error occurs when using `instanceof` with built-in types like String, Number, or Boolean, which doesn't work as expected because JavaScript has both primitive values and object wrappers. The fix involves using `typeof` checks for primitives or understanding when `instanceof` actually works with built-in types.

Quick fix

Fixes Cannot narrow type with instanceof for built-in types

// WRONG - won't work for primitive strings
function processValue(value: unknown) {
  if (value instanceof String) {
    console.log(value.toUpperCase()); // TypeScript may not narrow correctly
  }
}

// CORRECT - use typeof for primitives
function processValue(value: unknown) {
  if (typeof value === "string") {
    console.log(value.toUpperCase()); // TypeScript correctly narrows to string
  }
}

// Other primitive type checks:
if (typeof value === "number") { /* value is number */ }
if (typeof value === "boolean") { /* value is boolean */ }
if (typeof value === "symbol") { /* value is symbol */ }
if (typeof value === "bigint") { /* value is bigint */ }
if (typeof value === "undefined") { /* value is undefined */ }
  1. Use `typeof` instead of `instanceof` for primitive values
  2. Understand when `instanceof` actually works with built-in types
  3. Create custom type guards for complex type checking
  4. Handle cross-realm `instanceof` issues
typescript — error
// WRONG - won't work for primitive strings
function processValue(value: unknown) {
if (value instanceof String) {
console.log(value.toUpperCase()); // TypeScript may not narrow correctly
}
}
// CORRECT - use typeof for primitives
function processValue(value: unknown) {
if (typeof value === "string") {
console.log(value.toUpperCase()); // TypeScript correctly narrows to string
}
}
// Other primitive type checks:
if (typeof value === "number") { /* value is number */ }
if (typeof value === "boolean") { /* value is boolean */ }
if (typeof value === "symbol") { /* value is symbol */ }
if (typeof value === "bigint") { /* value is bigint */ }
if (typeof value === "undefined") { /* value is undefined */ }
Cannot narrow type with instanceof for built-in types

What this error means

The "Cannot narrow type with instanceof for built-in types" error appears when TypeScript's type narrowing doesn't work as expected with the `instanceof` operator on built-in JavaScript types like String, Number, Boolean, etc. This happens because JavaScript has a fundamental distinction between primitive values and their object wrappers: 1. **Primitive values**: Simple values like `"hello"`, `42`, `true` 2. **Object wrappers**: Objects created with `new String("hello")`, `new Number(42)`, `new Boolean(true)` The `instanceof` operator only works with objects, not primitives. When you have a primitive value, `instanceof String` returns `false` even though the value is a string. TypeScript's type narrowing system understands this limitation and may not narrow types correctly when using `instanceof` with built-in types, leading to confusing type errors or unexpected behavior in your code.

Typical symptoms

  • TypeScript doesn't narrow types after `instanceof String`, `instanceof Number`, or `instanceof Boolean` checks
  • Code shows type errors even after runtime type checking
  • `instanceof` returns `false` for primitive values that should match the type
  • Type guards with built-in types don't work as expected
  • IDE shows incorrect type information after `instanceof` checks
  • Compilation succeeds but runtime behavior is incorrect

Common causes

  1. Using `instanceof` with primitive values (strings, numbers, booleans) instead of object wrappers
  2. Confusing JavaScript's `typeof` operator with TypeScript's type narrowing
  3. Expecting `instanceof` to work like `typeof` for built-in types
  4. Not understanding the difference between primitive values and object wrappers
  5. Using `instanceof` with values that could be either primitive or object wrapper
  6. Cross-realm issues where built-in constructors from different contexts aren't equal
  7. TypeScript's conservative type narrowing for built-in `instanceof` checks

How to fix "Cannot narrow type with instanceof for built-in types"

1Use `typeof` instead of `instanceof` for primitive values

For primitive values (strings, numbers, booleans, symbols, bigints, undefined), use typeof instead of instanceof:

typescript
// WRONG - won't work for primitive strings
function processValue(value: unknown) {
  if (value instanceof String) {
    console.log(value.toUpperCase()); // TypeScript may not narrow correctly
  }
}

// CORRECT - use typeof for primitives
function processValue(value: unknown) {
  if (typeof value === "string") {
    console.log(value.toUpperCase()); // TypeScript correctly narrows to string
  }
}

// Other primitive type checks:
if (typeof value === "number") { /* value is number */ }
if (typeof value === "boolean") { /* value is boolean */ }
if (typeof value === "symbol") { /* value is symbol */ }
if (typeof value === "bigint") { /* value is bigint */ }
if (typeof value === "undefined") { /* value is undefined */ }

Remember: typeof null === "object" (a JavaScript quirk), so use value === null to check for null.

2Understand when `instanceof` actually works with built-in types

instanceof only works with object wrappers created with new:

typescript
// Primitive string - instanceof doesn't work
const primitiveStr = "hello";
console.log(primitiveStr instanceof String); // false
console.log(typeof primitiveStr); // "string"

// Object wrapper string - instanceof works
const objectStr = new String("hello");
console.log(objectStr instanceof String); // true
console.log(typeof objectStr); // "object"

// In practice, you rarely create object wrappers manually
// Most built-in types you work with are primitives

For arrays, dates, and custom classes, instanceof works as expected:

typescript
const arr = [1, 2, 3];
console.log(arr instanceof Array); // true

const date = new Date();
console.log(date instanceof Date); // true

class MyClass {}
const obj = new MyClass();
console.log(obj instanceof MyClass); // true
3Create custom type guards for complex type checking

For more complex type checking, create custom type guard functions:

typescript
// Custom type guard for string (primitive or object wrapper)
function isString(value: unknown): value is string {
  return typeof value === "string" || value instanceof String;
}

// Custom type guard for number
function isNumber(value: unknown): value is number {
  return typeof value === "number" || value instanceof Number;
}

// Usage
function processValue(value: unknown) {
  if (isString(value)) {
    // TypeScript knows value is string
    console.log(value.toUpperCase());
  }
}

// For union types
type StringOrNumber = string | number | String | Number;

function processUnion(value: StringOrNumber) {
  if (typeof value === "string" || value instanceof String) {
    // Handle string
  } else if (typeof value === "number" || value instanceof Number) {
    // Handle number
  }
}

Custom type guards give you precise control over type narrowing.

4Handle cross-realm `instanceof` issues

When dealing with multiple execution contexts (iframes, different windows), instanceof can fail:

typescript
// In an iframe or different window
const iframe = document.createElement("iframe");
document.body.appendChild(iframe);
const iframeArray = iframe.contentWindow?.Array;

const myArray = [1, 2, 3];

// This might be false in some contexts
console.log(myArray instanceof Array); // true in same realm
console.log(myArray instanceof iframeArray); // false - different constructors

// Solution: Use Array.isArray() or duck typing
console.log(Array.isArray(myArray)); // true - works across realms
console.log(Object.prototype.toString.call(myArray) === "[object Array]"); // true

For built-in types, use these cross-realm-safe checks:

typescript
// Array
Array.isArray(value)

// Date
Object.prototype.toString.call(value) === "[object Date]"

// RegExp
Object.prototype.toString.call(value) === "[object RegExp]"

// Error
Object.prototype.toString.call(value) === "[object Error]"
5Use TypeScript's built-in type predicates and narrowing

TypeScript provides several built-in type narrowing patterns:

typescript
// Type assertion with custom error
function assertIsString(value: unknown): asserts value is string {
  if (typeof value !== "string") {
    throw new Error("Value must be a string");
  }
}

// Usage
function process(value: unknown) {
  assertIsString(value);
  // value is now string
  console.log(value.toUpperCase());
}

// Discriminated unions
type Success = { type: "success"; data: string };
type Error = { type: "error"; message: string };
type Result = Success | Error;

function handleResult(result: Result) {
  if (result.type === "success") {
    // result is Success
    console.log(result.data);
  } else {
    // result is Error
    console.error(result.message);
  }
}

// in operator narrowing
type Circle = { kind: "circle"; radius: number };
type Square = { kind: "square"; sideLength: number };
type Shape = Circle | Square;

function getArea(shape: Shape) {
  if ("radius" in shape) {
    // shape is Circle
    return Math.PI * shape.radius ** 2;
  } else {
    // shape is Square
    return shape.sideLength ** 2;
  }
}
6Configure TypeScript for stricter type checking

Enable stricter TypeScript options to catch type issues earlier:

json
// tsconfig.json
{
  "compilerOptions": {
    "strict": true,
    "noImplicitAny": true,
    "strictNullChecks": true,
    "strictFunctionTypes": true,
    "strictBindCallApply": true,
    "strictPropertyInitialization": true,
    "noImplicitThis": true,
    "useUnknownInCatchVariables": true,
    "alwaysStrict": true,
    "exactOptionalPropertyTypes": true,
    "noUncheckedIndexedAccess": true,
    "noImplicitReturns": true,
    "noFallthroughCasesInSwitch": true,
    "noUnusedLocals": true,
    "noUnusedParameters": true,
    "allowUnreachableCode": false,
    "allowUnusedLabels": false
  }
}

With strict mode, TypeScript will be more careful about type narrowing and catch potential issues with instanceof and other type guards.

Also consider using the @typescript-eslint plugin with rules like:
- @typescript-eslint/no-unnecessary-type-assertion
- @typescript-eslint/no-unnecessary-condition
- @typescript-eslint/strict-boolean-expressions
- @typescript-eslint/prefer-nullish-coalescing
- @typescript-eslint/prefer-optional-chain

Advanced notes

### The Primitive vs Object Wrapper Distinction

JavaScript's design has historical reasons for having both primitives and object wrappers:

1. Primitives are immutable, lightweight values
- Stored directly in variables
- Passed by value
- Cannot have properties added
- "hello".prop = "value" does nothing (or throws in strict mode)

2. Object wrappers are full objects
- Created with new String(), new Number(), etc.
- Passed by reference
- Can have properties added
- Much heavier memory footprint

Auto-boxing: When you call a method on a primitive (like "hello".toUpperCase()), JavaScript temporarily converts it to an object wrapper, calls the method, then discards the wrapper. This is why primitives can have methods despite not being objects.

### TypeScript's Type Narrowing Algorithm

TypeScript uses control flow analysis to narrow types. For instanceof checks:

1. For custom classes: TypeScript narrows to the class type
2. For built-in types: TypeScript is conservative because:
- instanceof might fail for primitives
- Cross-realm issues exist
- Object wrappers are rare in practice

The TypeScript team has debated improving instanceof narrowing for built-ins, but the edge cases make it complex.

### Alternative: Branded Types

For runtime type safety, consider branded types:

typescript
// Branded type pattern
type Brand<K, T> = K & { __brand: T };

type Email = Brand<string, "Email">;
type UserId = Brand<string, "UserId">;

function createEmail(email: string): Email {
  if (!email.includes("@")) {
    throw new Error("Invalid email");
  }
  return email as Email;
}

function sendEmail(to: Email, body: string) {
  // Can only pass Email branded strings here
}

const email = createEmail("test@example.com");
sendEmail(email, "Hello"); // OK
sendEmail("not-an-email", "Hello"); // Type error

### Performance Considerations

- typeof is extremely fast (native JavaScript operator)
- instanceof has to traverse prototype chain (slower)
- Custom type guards add function call overhead
- For hot code paths, consider inline checks

### Testing Type Guards

Always test your type guards with edge cases:

typescript
function isString(value: unknown): value is string {
  return typeof value === "string" || value instanceof String;
}

// Test cases
console.assert(isString("hello") === true);
console.assert(isString(new String("hello")) === true);
console.assert(isString(123) === false);
console.assert(isString(null) === false);
console.assert(isString(undefined) === false);
console.assert(isString({}) === false);
console.assert(isString([]) === false);

Frequently asked questions

What does "Cannot narrow type with instanceof for built-in types" mean?
The "Cannot narrow type with instanceof for built-in types" error appears when TypeScript's type narrowing doesn't work as expected with the instanceof operator on built-in JavaScript types like String, Number, Boolean, etc. This happens because JavaScript has a fundamental distinction between primitive values and their object wrappers: 1. Primitive values: Simple values like "hello", 42, true 2.
How do I fix "Cannot narrow type with instanceof for built-in types"?
This TypeScript error occurs when using instanceof with built-in types like String, Number, or Boolean, which doesn't work as expected because JavaScript has both primitive values and object wrappers. The fix involves using typeof checks for primitives or understanding when instanceof actually works with built-in types.
What causes "Cannot narrow type with instanceof for built-in types"?
Using instanceof with primitive values (strings, numbers, booleans) instead of object wrappers Confusing JavaScript's typeof operator with TypeScript's type narrowing Expecting instanceof to work like typeof for built-in types
Is the "Cannot narrow type with instanceof for built-in types" error serious?
It is a medium-severity error — more of a blocker than a risk. The steps above resolve it.

Official resources & further reading