instanceof & in Operators
TypeScript's type narrowing is one of its killer features: the compiler analyses your control flow
and automatically refines types inside branches. Two of the most powerful narrowing tools are the
instanceof and in operators — both from JavaScript, but deeply understood by the
TypeScript type checker.
The instanceof Operator
instanceof tests whether an object was created by a particular constructor. At runtime it walks
the prototype chain; TypeScript uses it to narrow the type inside the branch.
class Dog {
bark() { return 'Woof!' }
}
class Cat {
meow() { return 'Meow!' }
}
function makeNoise(animal: Dog | Cat) {
if (animal instanceof Dog) {
// TypeScript knows: animal is Dog here
console.log(animal.bark())
} else {
// TypeScript knows: animal is Cat here
console.log(animal.meow())
}
}instanceof branch is the constructor's instance type, not the original union. TypeScript removes the other branches from consideration.instanceof with Class Hierarchies
instanceof works correctly with inheritance. TypeScript narrows to the most specific matching
class in the hierarchy.
class Animal {
breathe() { return 'breathing' }
}
class Dog extends Animal {
bark() { return 'Woof!' }
}
class GoldenRetriever extends Dog {
fetch() { return 'fetching!' }
}
function handle(a: Animal) {
if (a instanceof GoldenRetriever) {
console.log(a.fetch()) // GoldenRetriever — has .fetch(), .bark(), .breathe()
} else if (a instanceof Dog) {
console.log(a.bark()) // Dog — has .bark(), .breathe()
} else {
console.log(a.breathe()) // just Animal
}
}instanceof Dog before instanceof GoldenRetriever, the GoldenRetriever branch becomes dead code because GoldenRetriever instances also satisfy instanceof Dog.The in Operator
The in operator tests whether a property exists on an object: 'prop' in obj.
TypeScript uses it to narrow union types based on which members have a given property.
This is invaluable when working with plain objects (no class hierarchy).
interface Circle {
kind: 'circle'
radius: number
}
interface Rectangle {
kind: 'rectangle'
width: number
height: number
}
type Shape = Circle | Rectangle
function area(shape: Shape): number {
if ('radius' in shape) {
// TypeScript narrows to Circle
return Math.PI * shape.radius ** 2
} else {
// TypeScript narrows to Rectangle
return shape.width * shape.height
}
}in with Optional Properties
in is especially useful when properties are optional. If one branch of a union has an optional
property and another doesn't have it at all, in cleanly separates them.
interface AdminUser {
name: string
role: 'admin'
permissions: string[]
}
interface GuestUser {
name: string
role: 'guest'
}
type User = AdminUser | GuestUser
function describeUser(user: User) {
if ('permissions' in user) {
// Narrowed to AdminUser
console.log(`Admin with ${user.permissions.length} permissions`)
} else {
// Narrowed to GuestUser
console.log('Guest user — limited access')
}
}
describeUser({ name: 'Alice', role: 'admin', permissions: ['read', 'write'] })
describeUser({ name: 'Bob', role: 'guest' })Admin with 2 permissions Guest user — limited access
Combining instanceof and in
Real-world code often mixes class instances and plain objects in the same union. You can combine both operators freely.
class ApiError {
constructor(
public statusCode: number,
public message: string,
) {}
}
interface NetworkTimeout {
type: 'timeout'
durationMs: number
}
type Failure = ApiError | NetworkTimeout | Error
function handleFailure(failure: Failure) {
if (failure instanceof ApiError) {
console.log(`API Error ${failure.statusCode}: ${failure.message}`)
} else if ('durationMs' in failure) {
// Narrowed to NetworkTimeout
console.log(`Timed out after ${failure.durationMs}ms`)
} else {
// What's left: Error
console.log(`Unexpected error: ${failure.message}`)
}
}Common Mistakes
Mistake | Problem | Fix |
|---|---|---|
instanceof on plain objects | Plain objects have no constructor — always false | Use the in operator instead |
in on null/undefined | Runtime TypeError: cannot use in with null | Guard with != null first |
Most-specific class check last | Subclass instances caught by parent check first | Always check narrowest type first |
in with number literals | in checks property names (strings/symbols) | Use string keys: "0" in arr |
Safe null handling with in
// instanceof handles null safely (returns false)
function checkDate(val: Date | null) {
if (val instanceof Date) {
return val.toISOString()
}
return 'no date'
}
// in does NOT handle null — throws TypeError at runtime!
function safePropCheck(val: { name: string } | null) {
// WRONG: if ('name' in val) — crashes when val is null
if (val !== null && 'name' in val) { // correct guard
return val.name
}
return 'anonymous'
}in operator throws a TypeError at runtime if the right-hand side isnull or undefined. Always guard with a null check first.instanceof is safer — it returns false for null.Real-World: Error Handling Pattern
// A robust error handler for a fetch wrapper
class HttpError extends Error {
constructor(
public status: number,
message: string,
) {
super(message)
this.name = 'HttpError'
}
}
class ValidationError extends Error {
constructor(
public field: string,
message: string,
) {
super(message)
this.name = 'ValidationError'
}
}
function handleError(err: unknown): string {
if (err instanceof HttpError) {
return `HTTP ${err.status}: ${err.message}`
}
if (err instanceof ValidationError) {
return `Validation failed on "${err.field}": ${err.message}`
}
if (err instanceof Error) {
return `Error: ${err.message}`
}
// err is unknown — could be a string throw
return `Unknown failure: ${String(err)}`
}instanceof in error handlers gives you full type safety when catching from try/catch (where the caught value is typed as unknown). This is far safer than casting with as.Quick Reference
Operator | Best for | Works with |
|---|---|---|
instanceof | Class instances, Error subclasses | Classes (have constructors) |
in | Plain objects, interface discrimination | Any object with properties |
typeof | Primitives (string, number, boolean) | Primitive values |
Discriminant field | Tagged unions, API responses | Objects with a shared literal field |