6.2 Type Guards
What Are Type Guards?
TypeScript infers types through static analysis, but sometimes you need to check the actual runtime value. A type guard is a runtime check that narrows the type inside a specific scope.
Type guards let you:
- Pull a concrete type out of a union.
- Safely use values of type
unknown. - Parse external API response data in a type-safe way.
Type guard = runtime check + compile-time type narrowing
Core Concepts
1. typeof Type Guard
The typeof operator checks primitive types: string, number, boolean, bigint, symbol, undefined, and function.
function processInput(input: string | number | boolean): string {
if (typeof input === 'string') {
// input is string inside this block
return input.toUpperCase();
}
if (typeof input === 'number') {
// input is number inside this block
return input.toFixed(2);
}
// input is boolean here
return input ? 'true' : 'false';
}
// Limitation of typeof: 'object' covers null, arrays, and plain objects
function checkObject(value: unknown): void {
if (typeof value === 'object') {
// value is object | null — a null check is still needed!
if (value !== null) {
console.log(Object.keys(value));
}
}
}
2. instanceof Type Guard
The instanceof operator checks whether a value is an instance of a class (constructor function).
class Dog {
bark(): void { console.log('Woof!'); }
}
class Cat {
meow(): void { console.log('Meow!'); }
}
function makeSound(animal: Dog | Cat): void {
if (animal instanceof Dog) {
animal.bark(); // narrowed to Dog
} else {
animal.meow(); // narrowed to Cat
}
}
// Commonly used in error handling
function handleError(error: unknown): string {
if (error instanceof Error) {
return error.message; // access Error.message safely
}
if (error instanceof TypeError) {
return `Type error: ${error.message}`;
}
return String(error);
}
3. in Operator Type Guard
The in operator checks whether a property exists on an object.
interface Fish {
swim(): void;
fins: number;
}
interface Bird {
fly(): void;
wings: number;
}
function move(animal: Fish | Bird): void {
if ('swim' in animal) {
// narrowed to Fish
animal.swim();
} else {
// narrowed to Bird
animal.fly();
}
}
// Works with optional properties too
interface BasicUser {
id: number;
name: string;
}
interface AdminUser {
id: number;
name: string;
adminLevel: number;
permissions: string[];
}
function getPermissions(user: BasicUser | AdminUser): string[] {
if ('permissions' in user) {
return user.permissions; // AdminUser
}
return []; // BasicUser
}
4. Equality Type Guard
Comparisons with ===, !==, ==, and != also narrow types.
type Direction = 'north' | 'south' | 'east' | 'west';
type ExtendedDirection = Direction | 'up' | 'down';
function isHorizontal(dir: ExtendedDirection): boolean {
if (dir === 'north' || dir === 'south') {
return false; // dir is 'north' | 'south'
}
if (dir === 'up' || dir === 'down') {
return false; // dir is 'up' | 'down'
}
return true; // dir is 'east' | 'west'
}
// null/undefined equality guard
function processValue(value: string | null | undefined): string {
if (value == null) {
// Both null and undefined are excluded (loose equality)
return 'No value';
}
return value.trim(); // narrowed to string
}
5. User-Defined Type Guards
Define a custom type guard by using value is Type as the return type.
interface Cat {
kind: 'cat';
meow(): void;
}
interface Dog {
kind: 'dog';
bark(): void;
}
type Pet = Cat | Dog;
// User-defined type guard — return type is 'pet is Cat'
function isCat(pet: Pet): pet is Cat {
return pet.kind === 'cat';
}
function interact(pet: Pet): void {
if (isCat(pet)) {
pet.meow(); // narrowed to Cat
} else {
pet.bark(); // narrowed to Dog
}
}
// Validating unknown values
function isString(value: unknown): value is string {
return typeof value === 'string';
}
function isNumber(value: unknown): value is number {
return typeof value === 'number' && !isNaN(value);
}
function isObject(value: unknown): value is Record<string, unknown> {
return typeof value === 'object' && value !== null && !Array.isArray(value);
}
6. Assertion Functions
An asserts value is Type function throws when the condition fails. Code after the call sees the narrowed type.
function assert(condition: boolean, message: string): asserts condition {
if (!condition) throw new Error(message);
}
function assertIsString(value: unknown): asserts value is string {
if (typeof value !== 'string') {
throw new TypeError(`Not a string: ${typeof value}`);
}
}
function assertIsDefined<T>(value: T | null | undefined): asserts value is T {
if (value === null || value === undefined) {
throw new Error('Value is null or undefined.');
}
}
// Usage
function processConfig(config: unknown): void {
assertIsString(config); // config is string after this line
console.log(config.toUpperCase()); // safe to use
}
const maybeUser: User | null = getUser();
assertIsDefined(maybeUser); // maybeUser is User after this line
console.log(maybeUser.name); // no null check needed
Code Examples
Example 1: Combining Multiple Type Guards
interface Rectangle {
width: number;
height: number;
}
interface Circle {
radius: number;
}
interface Triangle {
base: number;
height: number;
}
type Shape = Rectangle | Circle | Triangle;
function isRectangle(shape: Shape): shape is Rectangle {
return 'width' in shape && 'height' in shape;
}
function isCircle(shape: Shape): shape is Circle {
return 'radius' in shape;
}
function isTriangle(shape: Shape): shape is Triangle {
return 'base' in shape && 'height' in shape;
}
function getArea(shape: Shape): number {
if (isCircle(shape)) return Math.PI * shape.radius ** 2;
if (isRectangle(shape)) return shape.width * shape.height;
if (isTriangle(shape)) return (shape.base * shape.height) / 2;
// All cases handled — TypeScript infers never here
const _: never = shape;
return 0;
}
Example 2: Array Type Guards
// Check element types in an array
function isStringArray(value: unknown): value is string[] {
return Array.isArray(value) && value.every(item => typeof item === 'string');
}
function isNumberArray(value: unknown): value is number[] {
return Array.isArray(value) && value.every(item => typeof item === 'number');
}
// Generic array type guard factory
function isArrayOf<T>(
guard: (item: unknown) => item is T
): (value: unknown) => value is T[] {
return (value): value is T[] =>
Array.isArray(value) && value.every(guard);
}
// Usage
const isUserArray = isArrayOf(
(item): item is User =>
isObject(item) &&
typeof (item as any).id === 'number' &&
typeof (item as any).name === 'string'
);
const data: unknown = [{ id: 1, name: 'Alice', email: 'alice@example.com' }];
if (isUserArray(data)) {
data.forEach(user => console.log(user.name)); // safe to use
}
Practical Examples
Example 1: API Response Parsing
interface ApiSuccess<T> {
status: 'success';
data: T;
timestamp: string;
}
interface ApiError {
status: 'error';
message: string;
code: number;
}
type ApiResponse<T> = ApiSuccess<T> | ApiError;
// User-defined type guards
function isApiSuccess<T>(response: ApiResponse<T>): response is ApiSuccess<T> {
return response.status === 'success';
}
function isApiError<T>(response: ApiResponse<T>): response is ApiError {
return response.status === 'error';
}
// Structure validation type guard
function isValidUser(value: unknown): value is User {
if (!isObject(value)) return false;
const obj = value as Record<string, unknown>;
return (
typeof obj.id === 'number' &&
typeof obj.name === 'string' &&
typeof obj.email === 'string' &&
obj.email.includes('@')
);
}
// API client
async function fetchUser(id: number): Promise<User> {
const raw = await fetch(`/api/users/${id}`).then(r => r.json());
// Receive as unknown, then validate with a type guard
const response = raw as ApiResponse<unknown>;
if (isApiError(response)) {
throw new Error(`API error ${response.code}: ${response.message}`);
}
if (!isValidUser(response.data)) {
throw new TypeError('API returned invalid user data.');
}
return response.data; // narrowed to User
}
// Batch processing
async function fetchUsers(ids: number[]): Promise<User[]> {
const results = await Promise.allSettled(ids.map(fetchUser));
return results
.filter((r): r is PromiseFulfilledResult<User> => r.status === 'fulfilled')
.map(r => r.value);
}
Example 2: Form Input Validation System
// Validation result types
interface ValidationSuccess {
valid: true;
value: string;
}
interface ValidationFailure {
valid: false;
errors: string[];
}
type ValidationResult = ValidationSuccess | ValidationFailure;
// Validator function type
type Validator = (value: string) => string | null; // null = pass, string = error message
// Validator factories
const validators = {
required: (label: string): Validator =>
(value) => value.trim() ? null : `${label} is required.`,
minLength: (min: number): Validator =>
(value) => value.length >= min ? null : `Must be at least ${min} characters.`,
maxLength: (max: number): Validator =>
(value) => value.length <= max ? null : `Must be at most ${max} characters.`,
email: (): Validator =>
(value) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value) ? null : 'Not a valid email address.',
pattern: (regex: RegExp, message: string): Validator =>
(value) => regex.test(value) ? null : message,
};
function validate(value: string, rules: Validator[]): ValidationResult {
const errors = rules
.map(rule => rule(value))
.filter((error): error is string => error !== null); // type guard!
if (errors.length === 0) {
return { valid: true, value: value.trim() };
}
return { valid: false, errors };
}
// Type guard to check the result
function isValid(result: ValidationResult): result is ValidationSuccess {
return result.valid;
}
// Form processing
interface LoginForm {
email: string;
password: string;
}
function processLoginForm(form: LoginForm): void {
const emailResult = validate(form.email, [
validators.required('Email'),
validators.email(),
]);
const passwordResult = validate(form.password, [
validators.required('Password'),
validators.minLength(8),
validators.maxLength(100),
]);
if (!isValid(emailResult)) {
console.error('Email errors:', emailResult.errors);
return;
}
if (!isValid(passwordResult)) {
console.error('Password errors:', passwordResult.errors);
return;
}
// Both results are narrowed to ValidationSuccess
console.log(`Login attempt: ${emailResult.value}`);
}
Example 3: Plugin System
// Plugin interface hierarchy
interface BasePlugin {
name: string;
version: string;
}
interface StoragePlugin extends BasePlugin {
type: 'storage';
read(key: string): Promise<string | null>;
write(key: string, value: string): Promise<void>;
}
interface AuthPlugin extends BasePlugin {
type: 'auth';
login(credentials: { username: string; password: string }): Promise<string>;
logout(token: string): Promise<void>;
verify(token: string): Promise<boolean>;
}
interface LogPlugin extends BasePlugin {
type: 'log';
log(level: 'info' | 'warn' | 'error', message: string): void;
getHistory(): string[];
}
type Plugin = StoragePlugin | AuthPlugin | LogPlugin;
// Type guards
function isStoragePlugin(plugin: Plugin): plugin is StoragePlugin {
return plugin.type === 'storage';
}
function isAuthPlugin(plugin: Plugin): plugin is AuthPlugin {
return plugin.type === 'auth';
}
function isLogPlugin(plugin: Plugin): plugin is LogPlugin {
return plugin.type === 'log';
}
// Plugin registry
class PluginRegistry {
private plugins: Map<string, Plugin> = new Map();
register(plugin: Plugin): void {
this.plugins.set(plugin.name, plugin);
}
getStorage(): StoragePlugin | undefined {
for (const plugin of this.plugins.values()) {
if (isStoragePlugin(plugin)) return plugin;
}
return undefined;
}
getAuth(): AuthPlugin | undefined {
for (const plugin of this.plugins.values()) {
if (isAuthPlugin(plugin)) return plugin;
}
return undefined;
}
getLogger(): LogPlugin | undefined {
for (const plugin of this.plugins.values()) {
if (isLogPlugin(plugin)) return plugin;
}
return undefined;
}
async initialize(): Promise<void> {
const logger = this.getLogger();
for (const [name, plugin] of this.plugins) {
logger?.log('info', `Initializing plugin: ${name} v${plugin.version}`);
}
}
}
Pro Tips
Tip 1: isNonNullable Pattern
// Type guard that filters out both null and undefined
function isNonNullable<T>(value: T | null | undefined): value is T {
return value !== null && value !== undefined;
}
// Usage: remove null values from an array
const maybeUsers: (User | null | undefined)[] = [
{ id: 1, name: 'Alice', email: 'alice@test.com' },
null,
{ id: 2, name: 'Bob', email: 'bob@test.com' },
undefined,
];
// filter's return type is automatically inferred as User[]
const users: User[] = maybeUsers.filter(isNonNullable);
Tip 2: Reusable Type Guards with Generics
// Generic guard that checks whether an object has a specific key
function hasProperty<K extends string>(
obj: unknown,
key: K
): obj is Record<K, unknown> {
return isObject(obj) && key in (obj as object);
}
function hasPropertyOfType<K extends string, V>(
obj: unknown,
key: K,
guard: (v: unknown) => v is V
): obj is Record<K, V> {
return hasProperty(obj, key) && guard((obj as any)[key]);
}
// Usage
function isProduct(value: unknown): value is { id: number; price: number; name: string } {
return (
hasPropertyOfType(value, 'id', isNumber) &&
hasPropertyOfType(value, 'price', isNumber) &&
hasPropertyOfType(value, 'name', isString)
);
}
Tip 3: Assertion Functions with Option Types
// Combining Option/Maybe pattern with assertion functions
type Option<T> = T | null | undefined;
function assertSome<T>(
option: Option<T>,
message = 'Value does not exist.'
): asserts option is T {
if (option === null || option === undefined) {
throw new Error(message);
}
}
function assertType<T>(
value: unknown,
guard: (v: unknown) => v is T,
message = 'Incorrect type.'
): asserts value is T {
if (!guard(value)) {
throw new TypeError(message);
}
}
// Usage
function processEnvVar(name: string): string {
const value = process.env[name];
assertSome(value, `Environment variable ${name} is not set.`);
return value; // narrowed to string
}
Tip 4: Combining Type Guards with filter
// Handling Promise.allSettled results
async function settledResults<T>(promises: Promise<T>[]): Promise<{
fulfilled: T[];
rejected: Error[];
}> {
const results = await Promise.allSettled(promises);
const fulfilled = results
.filter((r): r is PromiseFulfilledResult<T> => r.status === 'fulfilled')
.map(r => r.value);
const rejected = results
.filter((r): r is PromiseRejectedResult => r.status === 'rejected')
.map(r => r.reason instanceof Error ? r.reason : new Error(String(r.reason)));
return { fulfilled, rejected };
}
Summary Table
| Type Guard | Syntax | Target Types | Notes |
|---|---|---|---|
| typeof | typeof x === 'string' | Primitives | typeof null === 'object' — watch out |
| instanceof | x instanceof Dog | Class instances | Prototype-chain based; breaks after serialization |
| in | 'prop' in x | Object properties | Cannot be applied to null/undefined |
| Equality | x === 'value' | Literal types | == null idiom for null + undefined |
| User-defined | value is Type return | Any type | Correctness of the guard body is the developer's responsibility |
| Assertion | asserts value is Type | Any type | Must throw on failure |
| isNonNullable | value is T (excludes null/undefined) | Nullable types | Often combined with array filter |
Next we will look at Mapped Types (6.3). You will learn the powerful technique of transforming existing types with the { [K in keyof T]: ... } syntax and implement utility types such as Partial, Readonly, and Pick yourself.