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.
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 */ }// 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
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);