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.
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.
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
### 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("[email protected]");
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);Function expression requires a return type
Function expression requires a return type
Value of type 'string | undefined' is not iterable
How to fix "Value is not iterable" in TypeScript
Type 'undefined' is not assignable to type 'string'
How to fix "Type undefined is not assignable to type string" in TypeScript
Type narrowing from typeof check produces 'never'
How to fix "Type narrowing produces never" in TypeScript
Type parameter 'T' has conflicting constraints
How to fix "Type parameter has conflicting constraints" in TypeScript