How to fix Cannot narrow type with instanceof for built-in types in TypeScript
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 */ }- Use `typeof` instead of `instanceof` for primitive values
- Understand when `instanceof` actually works with built-in types
- Create custom type guards for complex type checking
- Handle cross-realm `instanceof` issues
// WRONG - won't work for primitive stringsfunction processValue(value: unknown) {if (value instanceof String) {console.log(value.toUpperCase()); // TypeScript may not narrow correctly}}// CORRECT - use typeof for primitivesfunction 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
- 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
- Not understanding the difference between primitive values and object wrappers
- Using `instanceof` with values that could be either primitive or object wrapper
- Cross-realm issues where built-in constructors from different contexts aren't equal
- TypeScript's conservative type narrowing for built-in `instanceof` checks
How to fix "Cannot narrow type with instanceof for built-in types"
For primitive values (strings, numbers, booleans, symbols, bigints, undefined), use typeof instead of instanceof:
// 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.
instanceof only works with object wrappers created with new:
// 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 primitivesFor arrays, dates, and custom classes, instanceof works as expected:
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); // trueFor more complex type checking, create custom type guard functions:
// 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.
When dealing with multiple execution contexts (iframes, different windows), instanceof can fail:
// 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]"); // trueFor built-in types, use these cross-realm-safe checks:
// 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]"TypeScript provides several built-in type narrowing patterns:
// 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;
}
}Enable stricter TypeScript options to catch type issues earlier:
// 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:
// 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:
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
- 📚TypeScript Official Documentation
- 📖TypeScript Handbook - Narrowing(Official Docs)
- 📖TypeScript Handbook - typeof Types(Official Docs)
- 📖MDN - instanceof operator(Official Docs)
- 📖MDN - typeof operator(Official Docs)
- 🐙