TypeScript 14 ๐ท Type Guards โ typeof, instanceof, in
A type guard is a runtime check that tells TypeScript what type a value is. The three built-in guards are typeof (for primitives), instanceof (for class instances), and in (for object shapes). Each one performs a real JavaScript check at runtime, and TypeScript follows along โ narrowing the value’s type inside the corresponding branch. They’re the foundation of narrowing, and knowing which guard fits which situation is what turns a union from a source of errors into a source of safety.
Key point: A type guard is both a runtime check and a compile-time narrowing. typeof x === 'string' runs in JavaScript and returns a boolean โ but TypeScript also understands it and narrows x to string in the if branch. The check is real, the narrowing is automatic. That’s the whole idea: safety without manual assertions.
What a type guard is
A type guard is a check that determines the type of a value at runtime and informs the compiler’s view of that value.
function format(value: string | number): string {
if (typeof value === 'string') {
// value is string here
return value.toUpperCase();
}
// value is number here
return value.toFixed(2);
}
typeof value === 'string' is the type guard. At runtime, it’s an ordinary comparison. At compile time, TypeScript uses it to narrow value from string | number to string inside the branch, and to number afterward.
Three built-in type guards:
| Guard | Narrows | Use for |
|---|---|---|
typeof x === '...' | Primitives | string, number, boolean, etc. |
x instanceof C | Class instances | Classes, built-ins like Error, Date |
'prop' in x | Object shapes | Interfaces, discriminated unions |
Each guard has a specific runtime behavior and a specific narrowing effect. Choosing the right one is the whole skill.
What makes something a type guard (in TypeScript’s eyes): The check must be one TypeScript recognizes. typeof x === 'string' works. isString(x) (a plain function that returns a boolean) doesn’t โ unless it’s declared as a predicate with x is T. The three built-ins are recognized because their semantics are defined by JavaScript itself.
Why these three: They’re JavaScript’s native type-discriminating operators.
typeofinspects primitives,instanceofinspects the prototype chain,ininspects property presence. Together they cover the three shapes of runtime data โ primitives, class instances, and object structures. Any other narrowing must be expressed as a predicate, because TypeScript can’t infer arbitrary user logic.
typeof โ narrowing primitives
typeof returns a string describing the value’s primitive type.
typeof 'hello' // 'string'
typeof 42 // 'number'
typeof true // 'boolean'
typeof 10n // 'bigint'
typeof Symbol() // 'symbol'
typeof undefined // 'undefined'
typeof function(){} // 'function'
typeof {} // 'object'
typeof null // 'object' โ the classic trap
The full mapping:
typeof result | TypeScript narrows to |
|---|---|
'string' | string |
'number' | number |
'boolean' | boolean |
'bigint' | bigint |
'symbol' | symbol |
'undefined' | undefined |
'function' | Function |
'object' | object | null |
typeof null === 'object' โ a legacy quirk of JavaScript that TypeScript accounts for. Narrowing with typeof x === 'object' gives object | null, so you must handle null separately.
Narrowing primitives:
function process(value: string | number | boolean): string {
if (typeof value === 'string') return value.toUpperCase();
if (typeof value === 'number') return value.toFixed(2);
return value ? 'yes' : 'no'; // value is boolean
}
Each branch narrows value to the matched primitive.
Narrowing undefined and null:
function greet(name: string | undefined): string {
if (typeof name === 'undefined') return 'Hello, stranger';
return `Hello, ${name}`; // name is string
}
typeof x === 'undefined' works even if the variable might not exist. But x === undefined is more direct โ use it when the variable is definitely declared.
Narrowing 'object' requires a null check:
function process(value: string | object | null): string {
if (typeof value === 'object') {
// value is object | null โ not just object
if (value === null) return 'null';
return Object.keys(value).join(','); // value is object
}
return value; // value is string
}
typeof value === 'object' && value !== null is a common combined check.
What typeof cannot narrow:
interface User { name: string; }
interface Admin { role: string; }
function f(x: User | Admin): void {
if (typeof x === 'object') {
// x is still User | Admin โ typeof object doesn't distinguish interfaces
}
}
typeof distinguishes primitives, not object shapes. For interfaces, use in.
Why
typeofis the primitive tool: JavaScript’stypeofwas designed to inspect primitives. It can’t tell two object types apart โ that’s not what it does. TypeScript respects this:typeofnarrows primitives precisely, but leaves object types alone. When you’re checking forstringvsnumbervsboolean, usetypeof. When you’re distinguishing two interfaces, usein.
instanceof โ narrowing class instances
instanceof checks whether a value was created by a specific class.
const date = new Date();
date instanceof Date; // true
const error = new Error('x');
error instanceof Error; // true
error instanceof TypeError; // false
It walks the prototype chain, so subclasses match their parent classes too:
class Animal {}
class Dog extends Animal {}
const d = new Dog();
d instanceof Dog; // true
d instanceof Animal; // true
d instanceof Object; // true
Narrowing:
class HttpError extends Error {
statusCode: number;
constructor(msg: string, code: number) {
super(msg);
this.statusCode = code;
}
}
function handle(err: unknown): string {
if (err instanceof HttpError) {
return `${err.statusCode}: ${err.message}`; // err is HttpError
}
if (err instanceof Error) {
return err.message; // err is Error
}
if (typeof err === 'string') {
return err; // err is string
}
return 'Unknown error';
}
instanceof narrows to the class โ err becomes HttpError, Error, or string depending on the branch.
Built-in types worth checking:
| Constructor | Narrows to |
|---|---|
Error | Error |
Date | Date |
RegExp | RegExp |
Map | Map<unknown, unknown> |
Set | Set<unknown> |
Array | unknown[] (usually use Array.isArray) |
Promise | Promise<unknown> |
instanceof only works for classes, not interfaces:
interface User { name: string; }
const data: unknown = { name: 'Alice' };
data instanceof User; // โ User is a type, not a value
Interfaces and type aliases don’t exist at runtime. instanceof needs a constructor function โ a class, or a built-in like Error. For interfaces, use in or a predicate.
instanceof with subclasses:
if (err instanceof HttpError) {
// err is HttpError โ has both Error and HttpError properties
}
A subclass instance is an instance of its superclass too, so instanceof HttpError also implies instanceof Error. TypeScript narrows to the more specific class in that branch.
Error handling is the classic use case:
try {
await doWork();
} catch (err) {
// err is unknown under strict
if (err instanceof Error) {
console.error(err.message); // err is Error
} else if (typeof err === 'string') {
console.error(err); // err is string
}
}
catch variables are unknown under strict. instanceof Error is how you narrow them.
Why
instanceofbeatstypeoffor classes:typeof x === 'object'matches every object โ anError, aDate, aMap, all give'object'.instanceofwalks the prototype chain and tells you which class. Usetypeoffor primitives,instanceoffor classes. And remember:instanceofonly works for runtime values โ classes and built-ins, never interfaces.
in โ narrowing object shapes
The in operator checks whether a property exists on an object.
'name' in { name: 'Alice' } // true
'age' in { name: 'Alice' } // false
As a type guard, in narrows to the union members that have the property.
interface Dog {
bark: () => void;
name: string;
}
interface Cat {
meow: () => void;
name: string;
}
function speak(pet: Dog | Cat): void {
if ('bark' in pet) {
pet.bark(); // pet is Dog
} else {
pet.meow(); // pet is Cat
}
}
'bark' in pet tells TypeScript that pet has a bark property โ which only Dog has โ so it narrows to Dog.
Multiple types with the property: in narrows to the union of types that have it.
type A = { a: number };
type B = { b: string };
type C = { a: number; b: string };
function f(x: A | B | C): void {
if ('a' in x) {
// x is A | C โ only these have `a`
}
if ('b' in x) {
// x is B | C โ only these have `b`
}
}
Both checks together would narrow to C:
if ('a' in x && 'b' in x) {
// x is C
}
in on optional properties:
interface User {
id: number;
nickname?: string;
}
function greet(u: User): string {
if ('nickname' in u) {
// Without exactOptionalPropertyTypes:
// nickname is string
// With exactOptionalPropertyTypes:
// nickname is string | undefined
return `Hi, ${u.nickname ?? 'user'}`;
}
return `Hi, user #${u.id}`;
}
in checks presence, not value. An optional property present with undefined still passes the check. Use ?? afterward to handle undefined.
in on class instances:
if ('statusCode' in err) {
// err has a statusCode property
}
in works on any object โ including instances โ as long as the property exists at runtime.
in on arrays:
'length' in [1, 2, 3]; // true
Arrays have length, push, etc. in works but is usually the wrong tool for arrays โ use Array.isArray instead.
in distinguishes interfaces structurally:
type Success = { data: string };
type Failure = { error: string };
function handle(r: Success | Failure): string {
if ('data' in r) return r.data;
return r.error;
}
Without a shared discriminant, in on the distinguishing property narrows the union.
When in is the tool: Any time you’re narrowing between object shapes that differ by property names. Common in discriminated unions, response handling, and structural type dispatch.
Why
inmatters for interfaces: Interfaces are structural โ they exist only in the type system.instanceofcan’t see them;typeofreturns'object'for all of them. The only runtime evidence of an interface’s identity is its properties.inreads those properties. That’s whyinis the standard way to narrow interfaces and type aliases.
Comparing the three guards
| Guard | Works on | Narrows to |
|---|---|---|
typeof | Primitives | string, number, boolean, etc. |
instanceof | Class instances | A specific class |
in | Any object | Union members with the property |
When to use which:
| Situation | Guard |
|---|---|
Checking for string, number, etc. | typeof |
| Checking for a class instance | instanceof |
| Checking for a specific interface | in |
Checking for Error | instanceof Error |
Checking for Date | instanceof Date |
Checking for Map | instanceof Map |
| Discriminated union | switch on discriminant |
| Array | Array.isArray |
| Optional property | in (or !== undefined) |
null check | === null or == null |
Combining guards:
function handle(x: string | number | Error | User): string {
if (typeof x === 'string') return x;
if (typeof x === 'number') return x.toFixed(2);
if (x instanceof Error) return x.message;
// x is User
return x.name;
}
Each check narrows further. By the end, only User remains.
Order matters with overlapping types:
class Base {}
class Derived extends Base {}
function f(x: Base | Derived): void {
if (x instanceof Base) {
// x is Base | Derived โ Derived still matches Base
}
if (x instanceof Derived) {
// x is Derived โ the narrower class
}
}
Check the most specific class first.
Why the three guards are complementary:
typeofhandles primitives,instanceofhandles classes,inhandles plain objects. There’s no overlap in their domains โ each has a specific runtime behavior. Together they cover every value JavaScript can produce. TypeScript recognizes all three because their semantics are well-defined by the language itself โ no user annotation needed.
Type guards vs type predicates
The three built-in guards cover many cases, but not all. When you need a custom check, use a type predicate.
function isString(value: unknown): value is string {
return typeof value === 'string';
}
function isUser(value: unknown): value is User {
return (
typeof value === 'object' &&
value !== null &&
'id' in value &&
typeof (value as { id: unknown }).id === 'number' &&
'name' in value &&
typeof (value as { name: unknown }).name === 'string'
);
}
A predicate combines any number of runtime checks and tells TypeScript the resulting type.
When built-in guards suffice: When the check is one of the three primitives โ typeof, instanceof, in.
When to reach for a predicate:
- Validating a complex shape (multiple properties, nested checks)
- Narrowing to an interface with structural checks
- Reusable checks across many call sites
- Narrowing across function boundaries
Predicates compose the built-ins:
function isNonEmptyString(x: unknown): x is string {
return typeof x === 'string' && x.length > 0;
}
function isPositive(x: unknown): x is number {
return typeof x === 'number' && x > 0 && Number.isFinite(x);
}
Predicates narrow further than built-ins:
function isCircle(s: Shape): s is { kind: 'circle'; radius: number } {
return s.kind === 'circle';
}
if (isCircle(shape)) {
// shape is { kind: 'circle'; radius: number }
}
The predicate narrows to a specific union member, which typeof alone couldn’t do.
Predicates are trusted: The compiler doesn’t verify the body โ it trusts the is T annotation. If the body returns true for values that aren’t T, the narrowing is wrong. Write predicates carefully.
Why predicates extend guards: Built-in guards are constrained to their runtime semantics โ
typeofcan only check primitives,incan only check property presence. Real types combine checks โ “a string with length > 0,” “an object with id and name of the right types.” Predicates express those compound checks and carry them through TypeScript’s narrowing. They’re the bridge between runtime validation and compile-time types.
Guarding at the boundary
Type guards are most useful at the edges of your program โ where untyped data enters.
From JSON.parse:
interface Config {
apiUrl: string;
timeout: number;
}
function isConfig(x: unknown): x is Config {
return (
typeof x === 'object' &&
x !== null &&
'apiUrl' in x &&
typeof (x as { apiUrl: unknown }).apiUrl === 'string' &&
'timeout' in x &&
typeof (x as { timeout: unknown }).timeout === 'number'
);
}
const raw: unknown = JSON.parse(input);
if (!isConfig(raw)) throw new Error('Invalid config');
// raw is Config
From a fetch response:
async function fetchUser(id: number): Promise<User> {
const res = await fetch(`/users/${id}`);
const data: unknown = await res.json();
if (!isUser(data)) throw new Error('Invalid user');
return data; // narrowed to User
}
From user input:
function parseNumber(input: unknown): number {
if (typeof input === 'number') return input;
if (typeof input === 'string') {
const n = Number(input);
if (Number.isFinite(n)) return n;
}
throw new Error('Not a number');
}
From DOM events:
function onClick(e: MouseEvent): void {
if (e.target instanceof HTMLButtonElement) {
e.target.disabled = true; // target is HTMLButtonElement
}
}
Using instanceof on DOM classes is often better than as.
From error catch blocks:
try {
await riskyOperation();
} catch (err) {
if (err instanceof Error) {
log(err.message);
} else if (typeof err === 'string') {
log(err);
} else {
log('Unknown error');
}
}
Why guards are boundary tools: Inside your own typed code, types are known. At the edges โ JSON, network, user input, DOM, errors โ data is untyped or
unknown. Guards are how you convert untrusted data into typed values. The pattern: validate at the boundary with a guard, then use the typed value everywhere inside. That’s the safety model.
Guard patterns for discriminated unions
The most common real-world use of type guards is narrowing discriminated unions.
type Message =
| { kind: 'text'; text: string }
| { kind: 'image'; url: string; alt: string }
| { kind: 'file'; name: string; size: number };
function render(m: Message): string {
switch (m.kind) {
case 'text': return m.text;
case 'image': return `[${m.alt}](${m.url})`;
case 'file': return `${m.name} (${m.size} bytes)`;
}
}
switch on the discriminant narrows m to each branch.
Guards for shared logic:
function isText(m: Message): m is { kind: 'text'; text: string } {
return m.kind === 'text';
}
const texts = messages.filter(isText);
// texts: { kind: 'text'; text: string }[]
Predicates compose with Array.filter to produce typed subsets.
Guards for compound checks:
type Result =
| { ok: true; value: string }
| { ok: false; error: string };
function isOk(r: Result): r is { ok: true; value: string } {
return r.ok;
}
if (isOk(result)) {
console.log(result.value); // value is string
} else {
console.log(result.error); // error is string
}
Guards with in for non-discriminated unions:
type Response =
| { data: string }
| { error: string };
function isSuccess(r: Response): r is { data: string } {
return 'data' in r;
}
Guards with exhaustiveness:
function assertNever(x: never): never {
throw new Error(`Unhandled: ${JSON.stringify(x)}`);
}
function handle(m: Message): string {
switch (m.kind) {
case 'text': return m.text;
case 'image': return m.url;
case 'file': return m.name;
default: return assertNever(m);
}
}
Combining predicates, in, typeof, and switch handles any union exhaustively.
Why guards and discriminated unions are paired: A discriminated union has a literal field that distinguishes its branches. Narrowing on that field โ via
switchor a predicate โ gives TypeScript the exact branch. That’s the cleanest narrowing pattern, and it’s why literal types and discriminants are so important. Guards are the mechanism; discriminated unions are the shape.
A full example
A response handler that uses all three guards plus a predicate.
// ============================================
// TYPES
// ============================================
interface ApiSuccess {
data: {
id: number;
name: string;
};
}
interface ApiError {
error: {
code: number;
message: string;
};
}
type ApiResponse = ApiSuccess | ApiError;
// ============================================
// PREDICATES
// ============================================
function isApiSuccess(r: unknown): r is ApiSuccess {
return (
typeof r === 'object' &&
r !== null &&
'data' in r &&
typeof (r as { data: unknown }).data === 'object'
);
}
function isApiError(r: unknown): r is ApiError {
return (
typeof r === 'object' &&
r !== null &&
'error' in r &&
typeof (r as { error: unknown }).error === 'object'
);
}
// ============================================
// HANDLERS
// ============================================
function extractMessage(r: ApiResponse): string {
if ('data' in r) {
return `Got ${r.data.name}`;
}
return `Error ${r.error.code}: ${r.error.message}`;
}
async function fetchApi(url: string): Promise<ApiResponse> {
const res = await fetch(url);
const json: unknown = await res.json();
if (isApiSuccess(json)) return json;
if (isApiError(json)) return json;
throw new Error('Unexpected response shape');
}
// ============================================
// ERROR HANDLING
// ============================================
function handleError(err: unknown): string {
if (err instanceof Error) return err.message;
if (typeof err === 'string') return err;
return 'Unknown error';
}
// ============================================
// USAGE
// ============================================
const success: ApiResponse = { data: { id: 1, name: 'Alice' } };
const failure: ApiResponse = { error: { code: 404, message: 'Not found' } };
console.log(extractMessage(success)); // Got Alice
console.log(extractMessage(failure)); // Error 404: Not found
console.log(handleError(new Error('oops')));
console.log(handleError('failed'));
console.log(handleError({ weird: true }));
Every guard type is used: in in extractMessage, predicates in isApiSuccess/isApiError, instanceof and typeof in handleError.
Why this pattern is realistic: It mirrors how real API clients work. A response arrives as
unknown, gets validated, becomes a typed value, and the rest of the code operates on that type. Errors from any source โError, strings, plain objects โ get normalized via guards. That’s the whole safety story at boundaries.
Complete Example Session
# ============================================
# PART 1: typeof
# ============================================
cat > typeof.ts << 'EOF'
function format(v: string | number | boolean): string {
if (typeof v === 'string') return v.toUpperCase();
if (typeof v === 'number') return v.toFixed(2);
return v ? 'yes' : 'no';
}
// typeof null trap
function process(v: string | object | null): string {
if (typeof v === 'object') {
// v is object | null โ must check null
if (v === null) return 'null';
return Object.keys(v).join(',');
}
return v;
}
console.log(format('hi'), format(3.14), format(true));
console.log(process(null), process({ a: 1, b: 2 }), process('x'));
EOF
npx tsc --noEmit typeof.ts
# (no errors)
# ============================================
# PART 2: instanceof
# ============================================
cat > instanceof.ts << 'EOF'
class HttpError extends Error {
constructor(msg: string, public status: number) { super(msg); }
}
function handle(err: unknown): string {
if (err instanceof HttpError) return `${err.status}: ${err.message}`;
if (err instanceof Error) return err.message;
if (typeof err === 'string') return err;
return 'Unknown';
}
console.log(handle(new HttpError('Not found', 404)));
console.log(handle(new Error('boom')));
console.log(handle('plain'));
console.log(handle({ code: 1 }));
// Built-ins
console.log(new Date() instanceof Date); // true
console.log(/x/ instanceof RegExp); // true
console.log(new Map() instanceof Map); // true
EOF
npx tsc --noEmit instanceof.ts
# (no errors)
# ============================================
# PART 3: in
# ============================================
cat > in.ts << 'EOF'
interface Dog { bark: () => void; name: string; }
interface Cat { meow: () => void; name: string; }
function speak(pet: Dog | Cat): string {
if ('bark' in pet) return pet.bark();
return pet.meow();
}
const dog: Dog = { name: 'Rex', bark: () => 'woof' };
const cat: Cat = { name: 'Felix', meow: () => 'meow' };
console.log(speak(dog), speak(cat));
// Non-discriminated union
type Success = { data: string };
type Failure = { error: string };
function handle(r: Success | Failure): string {
if ('data' in r) return r.data;
return r.error;
}
console.log(handle({ data: 'ok' }), handle({ error: 'bad' }));
EOF
npx tsc --noEmit in.ts
# (no errors)
# ============================================
# PART 4: COMBINING GUARDS
# ============================================
cat > combined.ts << 'EOF'
interface User { name: string; }
function describe(x: string | number | Error | User): string {
if (typeof x === 'string') return `string: ${x}`;
if (typeof x === 'number') return `number: ${x}`;
if (x instanceof Error) return `error: ${x.message}`;
// x is User
return `user: ${x.name}`;
}
console.log(describe('hi'));
console.log(describe(42));
console.log(describe(new Error('oops')));
console.log(describe({ name: 'Alice' }));
EOF
npx tsc --noEmit combined.ts
# (no errors)
# ============================================
# PART 5: PREDICATES FOR COMPLEX TYPES
# ============================================
cat > predicates.ts << 'EOF'
interface User { id: number; name: string; }
function isUser(x: unknown): x is User {
return (
typeof x === 'object' &&
x !== null &&
'id' in x &&
typeof (x as { id: unknown }).id === 'number' &&
'name' in x &&
typeof (x as { name: unknown }).name === 'string'
);
}
const raw: unknown = JSON.parse('{"id":1,"name":"Alice"}');
if (isUser(raw)) {
console.log(raw.name); // User
} else {
console.log('Invalid');
}
// Discriminated union
type Message =
| { kind: 'text'; text: string }
| { kind: 'image'; url: string };
function isText(m: Message): m is { kind: 'text'; text: string } {
return m.kind === 'text';
}
const messages: Message[] = [
{ kind: 'text', text: 'hello' },
{ kind: 'image', url: '/a.png' }
];
const texts = messages.filter(isText);
console.log(texts.map(t => t.text));
EOF
npx tsc --noEmit predicates.ts
# (no errors)
# ============================================
# PART 6: FULL EXAMPLE
# ============================================
cat > example.ts << 'EOF'
interface ApiSuccess { data: { id: number; name: string } }
interface ApiError { error: { code: number; message: string } }
type ApiResponse = ApiSuccess | ApiError;
function isApiSuccess(r: unknown): r is ApiSuccess {
return typeof r === 'object' && r !== null && 'data' in r;
}
function extractMessage(r: ApiResponse): string {
if ('data' in r) return `Got ${r.data.name}`;
return `Error ${r.error.code}: ${r.error.message}`;
}
function handleError(err: unknown): string {
if (err instanceof Error) return err.message;
if (typeof err === 'string') return err;
return 'Unknown error';
}
console.log(extractMessage({ data: { id: 1, name: 'Alice' } }));
console.log(extractMessage({ error: { code: 404, message: 'Not found' } }));
console.log(handleError(new Error('boom')));
console.log(handleError('oops'));
EOF
npx tsc --noEmit example.ts
# (no errors)
# ============================================
# PART 7: COMPILE AND RUN
# ============================================
npx tsc typeof.ts instanceof.ts in.ts combined.ts predicates.ts example.ts
node typeof.js
# [ HI 3.14 yes ]
# [ null a,b x ]
node instanceof.js
# [ 404: Not found ]
# [ boom ]
# [ plain ]
# [ Unknown ]
# [ true true true ]
node in.js
# [ woof meow ]
# [ ok bad ]
node combined.js
# [ string: hi ]
# [ number: 42 ]
# [ error: oops ]
# [ user: Alice ]
node predicates.js
# [ Alice ]
# [ [ 'hello' ] ]
node example.js
# [ Got Alice ]
# [ Error 404: Not found ]
# [ boom ]
# [ oops ]
Quick Reference
The Three Built-In Guards
| Guard | Narrows | Domain |
|---|---|---|
typeof x === '...' | Primitives | string, number, boolean, etc. |
x instanceof C | Class instances | Classes, built-ins |
'p' in x | Object shapes | Any object with the property |
typeof Results
| Result | Narrows to |
|---|---|
'string' | string |
'number' | number |
'boolean' | boolean |
'bigint' | bigint |
'symbol' | symbol |
'undefined' | undefined |
'function' | Function |
'object' | object | null |
instanceof with Built-Ins
| Constructor | Narrows to |
|---|---|
Error | Error |
Date | Date |
RegExp | RegExp |
Map | Map<unknown, unknown> |
Set | Set<unknown> |
Promise | Promise<unknown> |
ArrayBuffer | ArrayBuffer |
in Narrowing Behavior
| Situation | Result |
|---|---|
| Property only on A | Narrows to A |
| Property on A and B | Narrows to A | B |
| Property on all | No narrowing |
| Optional property | Property is present |
exactOptionalPropertyTypes | May still include undefined |
When to Use Which
| Situation | Guard |
|---|---|
| Check primitive type | typeof |
| Check class instance | instanceof |
| Check interface shape | in |
| Check array | Array.isArray |
| Discriminated union | switch on discriminant |
| Complex shape | Predicate |
| Null check | === null / == null |
| Optional property | in or !== undefined |
Combining Guards
| Pattern | Effect |
|---|---|
typeof x === 'object' && x !== null | Object, not null |
'data' in x && 'error' in x | Both properties |
x instanceof A && x instanceof B | Both classes |
typeof x === 'string' || typeof x === 'number' | Either primitive |
Guard vs Predicate vs Assertion
| Tool | Form | Runtime check |
|---|---|---|
| Built-in guard | typeof / instanceof / in | โ |
| Predicate | fn(x): x is T | โ (yours) |
| Assertion | x as T | โ |
| Assertion function | fn(x): asserts x is T | โ (throws) |
Common Narrowing Sequences
| Start | Checks | End |
|---|---|---|
unknown | typeof === 'string' | string |
Error | string | instanceof Error | Error or string |
A | B (different props) | 'a' in x | A |
| Discriminated union | switch (x.kind) | Specific branch |
T | null | !== null | T |
T | undefined | !== undefined | T |
T | null | undefined | != null | T |
Guards at Boundaries
| Source | Guard |
|---|---|
JSON.parse | Predicate on unknown |
| API response | Predicate after .json() |
| DOM event target | instanceof HTMLElement |
catch (err) | instanceof Error |
| User input | typeof + validation |
| File data | instanceof Blob etc. |
Best Practices
โ Do This:
// Use typeof for primitives
if (typeof x === 'string') { x.toUpperCase(); } // โ
// Use instanceof for errors and built-ins
if (err instanceof Error) { err.message; } // โ
// Use `in` for object shapes
if ('data' in response) { response.data; } // โ
// Combine typeof object and null check
if (typeof x === 'object' && x !== null) { Object.keys(x); } // โ
// Write predicates for complex shapes
function isUser(x: unknown): x is User { /* ... */ } // โ
// Validate at boundaries
const data: unknown = JSON.parse(raw);
if (isConfig(data)) { /* data is Config */ } // โ
// Use predicates with Array.filter
const texts = messages.filter(isText); // โ
// Narrow in catch blocks
try { } catch (err) {
if (err instanceof Error) { err.message; } // โ
}
// Check narrow classes first
if (x instanceof Derived) { } else if (x instanceof Base) { }// โ
โ Don’t Do This:
// Don't use typeof for interfaces
if (typeof x === 'object') { /* doesn't narrow to User */ } // โ ๏ธ
// Don't forget null with typeof object
if (typeof x === 'object') { Object.keys(x); } // โ if x can be null
// Don't use instanceof on interfaces
if (x instanceof User) { } // User isn't a value // โ
// Don't assert instead of guarding
const user = data as User; // โ ๏ธ no runtime check
// Don't skip validation at boundaries
const config = JSON.parse(raw) as Config; // โ ๏ธ unverified
// Don't use truthiness on 0 or ''
if (count) { } // skips 0 // โ ๏ธ
// Don't write predicates that lie
function isUser(x: unknown): x is User { return true; } // โ
// Don't order instanceof wrong
if (x instanceof Base) { } else if (x instanceof Derived) { }// โ ๏ธ Base catches Derived
Common Pitfalls
| Pitfall | Problem | Solution |
|---|---|---|
typeof null is 'object' | False positives | Check x !== null |
instanceof on interfaces | No runtime value | Use in or predicate |
typeof on object shapes | Stays union | Use in |
Truthiness skips 0/'' | Valid values dropped | !== undefined |
Order of instanceof | Base matches first | Specific classes first |
Optional property with in | May still be undefined | Use ?? |
| Predicate that lies | Runtime crash | Write correct check |
| Skipping boundary validation | Bad data flows in | Use a predicate |
| Asserting instead of guarding | No runtime check | Guard + narrow |
Guards on any | Silently narrows | Use unknown instead |
Real-World Examples
1. Primitive narrowing
if (typeof x === 'string') { x.toUpperCase(); }
2. Number narrowing
if (typeof x === 'number') { x.toFixed(2); }
3. Error narrowing
if (err instanceof Error) { err.message; }
4. Date narrowing
if (value instanceof Date) { value.toISOString(); }
5. Map narrowing
if (value instanceof Map) { value.get('key'); }
6. Property presence
if ('data' in response) { response.data; }
7. Discriminant property
if (msg.kind === 'text') { msg.text; }
8. Combined object + null check
if (typeof x === 'object' && x !== null) { Object.keys(x); }
9. Nullable check
if (x !== null) { x.name; }
10. Both null and undefined
if (x != null) { x.name; }
11. Array narrowing
if (Array.isArray(x)) { x.map(fn); }
12. Predicate for interface
function isUser(x: unknown): x is User { /* ... */ }
13. Predicate for discriminated union
function isText(m: Message): m is { kind: 'text'; text: string } {
return m.kind === 'text';
}
14. Predicate with Array.filter
const users = items.filter(isUser);
15. Boundary validation
const data: unknown = JSON.parse(raw);
if (isConfig(data)) { useConfig(data); }
16. Catch-block narrowing
try { } catch (err) {
if (err instanceof Error) log(err.message);
}
17. DOM event target
if (e.target instanceof HTMLInputElement) { e.target.value; }
18. Built-in guard combination
if (typeof x === 'number' && Number.isFinite(x)) { x.toFixed(2); }
19. Guard before using value
if ('name' in obj && typeof obj.name === 'string') { obj.name.length; }
20. Exhaustiveness with assertNever
default: return assertNever(msg);
Visual: Guard Decision Tree
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ What are you checking? โ
โ โ
โ โโโ Primitive type? โ typeof โ
โ โโโ Class instance? โ instanceof โ
โ โโโ Object shape? โ in โ
โ โโโ Array? โ Array.isArray โ
โ โโโ Discriminated union? โ switch on kind โ
โ โโโ Complex shape? โ predicate โ
โ โโโ Null/undefined? โ === / == null โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Visual: typeof Domain
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ typeof narrows โ
โ โ
โ string โ โ
โ number โ โ
โ boolean โ โ
โ bigint โ โ
โ symbol โ โ
โ undefined โ โ
โ function โ โ
โ object โ ๏ธ object | null โ
โ โ
โ Interfaces โ โ
โ Classes โ ๏ธ only as 'object' โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Visual: instanceof Domain
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ instanceof narrows โ
โ โ
โ Class instances โ โ
โ Subclasses โ (via proto chain) โ
โ Built-ins (Error) โ โ
โ Built-ins (Date) โ โ
โ โ
โ Interfaces โ (not values) โ
โ Type aliases โ โ
โ Primitives โ โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Visual: in Domain
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ in narrows โ
โ โ
โ Object interfaces โ โ
โ Type aliases โ โ
โ Class instances โ โ
โ Optional props โ (presence) โ
โ โ
โ Primitives โ (can't be used) โ
โ Arrays โ ๏ธ use Array.isArray โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Visual: Narrowing Through Branches
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ function handle(x: string | number | Error) โ
โ โ
โ if (typeof x === 'string') { โ
โ // x: string โ
โ } else if (typeof x === 'number') { โ
โ // x: number โ
โ } else { โ
โ // x: Error โ
โ } โ
โ โ
โ Each check removes the matched type from โ
โ the union in later branches โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Visual: Boundary Validation
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Untrusted data โ
โ โ
โ JSON / API / user input / DOM โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ
โผ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Type guard / predicate โ
โ โ
โ Runs at runtime โ
โ Narrows to a typed value โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ
โผ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Typed code โ
โ โ
โ Compiler knows the shape โ
โ Autocomplete, refactoring, safety โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Visual: Guard Comparison Table
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Guard Narrow Use for โ
โ โ
โ typeof Primitive string/number โ
โ instanceof Class Error, Date โ
โ in Shape Interfaces โ
โ switch Literal Discriminated โ
โ Array.isArray Array Array check โ
โ predicate Any Custom โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Visual: typeof null Trap
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ typeof null === 'object' โ
โ โ
โ if (typeof x === 'object') { โ
โ // x is object | null โ
โ // โ x might be null โ
โ } โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Combined check โ
โ โ
โ if (typeof x === 'object' && x !== null) { โ
โ // x is object โ
โ // โ
safe โ
โ } โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Visual: Predicates Extend Guards
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Built-in guard โ
โ โ
โ typeof x === 'string' โ
โ โ
โ Narrows to: string โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ
โ Combine
โผ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Predicate โ
โ โ
โ function isNonEmptyString(x: unknown) โ
โ : x is string { โ
โ return typeof x === 'string' โ
โ && x.length > 0; โ
โ } โ
โ โ
โ Narrows to: non-empty string โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Summary
| Concept | Meaning |
|---|---|
| Type guard | Runtime check that narrows |
typeof | Guard for primitives |
instanceof | Guard for class instances |
in | Guard for object shapes |
Array.isArray | Guard for arrays |
| Predicate | x is T โ custom guard |
| Assertion function | asserts x is T โ throws |
| Discriminated union | switch on a literal discriminant |
| Boundary validation | Guards at the edges of typed code |
Key takeaways:
- Type guards are runtime checks that narrow the compile-time type
typeofnarrows primitives โstring,number,boolean, etc.instanceofnarrows class instances โError,Date, custom classesinnarrows object shapes โ interfaces and type aliasestypeof nullis'object'โ always checkx !== nullwhen narrowing toobjectinstanceofdoesn’t work on interfaces โ they don’t exist at runtimeinis the standard way to narrow between interfaces- Predicates (
x is T) extend built-in guards for complex shapes - Guards are most valuable at boundaries โ JSON, API, DOM, errors, user input
- Use
Array.isArrayfor arrays, nottypeoforin - Order
instanceofchecks from most specific class to most general - Combine guards for compound narrowing
- For discriminated unions, switch on the discriminant literal
Remember: Type guards are how you convert runtime reality into compile-time knowledge. typeof for primitives, instanceof for classes, in for shapes โ each is a real JavaScript check that TypeScript recognizes and narrows. When the built-ins aren’t enough, write a predicate. Validate at the boundaries โ where data enters from the outside world โ and the rest of your code stays typed. That’s the whole pattern: check once, trust everywhere inside.
Stop using slow, ad-bloated tool sites! ๐คฎ
๐ Search “KandZ Tools” on Google to use many professional utilities for free.
KandZ.me is the ultimate minimalist hub for:
โ
Finance (Mortgage, Interest, Inflation)
โ
Tech (Base64, JSON, Dev Suite, IP)
โ
Health (BMI, BMR, TDEE)
โ
Productivity (Timer, Workspace, QR)
โก๏ธ Fast & Private
๐ No data leaves your device
๐ 100% Free
๐ Use it now: https://tools.kandz.me
๐ Bookmark itโyouโll need it later!