Type Assertions & Type Guards
Type Assertions & Type Guards
TypeScript provides powerful mechanisms to work with types at runtime. Type assertions allow you to tell the compiler "trust me, I know what I'm doing," while type guards let you narrow types safely based on runtime checks. Understanding these concepts is essential for writing flexible yet type-safe TypeScript code.
Type Assertions
Type assertions are a way to tell TypeScript that you know more about a value's type than it does. They don't perform any runtime checks or conversions; they're purely for the compiler.
// Basic type assertion
let someValue: unknown = 'Hello, TypeScript!';
// Assert that someValue is a string
let strLength: number = (someValue as string).length;
console.log(strLength); // 18
// Type assertion with DOM elements
const inputElement = document.getElementById('username') as HTMLInputElement;
inputElement.value = 'john_doe';
// Type assertion with fetch response
async function fetchUser(id: string): Promise<void> {
const response = await fetch(`/api/users/${id}`);
const data = await response.json();
// Assert the shape of the data
const user = data as {
id: string;
name: string;
email: string;
};
console.log(user.name);
}
// Type assertion with object literals
interface Point {
x: number;
y: number;
}
const point = { x: 10, y: 20, z: 30 } as Point;
// Note: 'z' property is ignored by the type system
// Multiple assertions (chain)
let value: unknown = '123';
let numValue = (value as unknown) as number; // Not recommended, but possible
Angle-Bracket Syntax (Alternative)
TypeScript also supports angle-bracket syntax for type assertions, though as is preferred in JSX/TSX files:
// Angle-bracket syntax (equivalent to 'as')
let someValue: unknown = 'Hello!';
let strLength: number = (<string>someValue).length;
// Both syntaxes are equivalent
let value1 = someValue as string;
let value2 = <string>someValue;
// However, 'as' syntax is required in .tsx files
// because <> conflicts with JSX syntax
as syntax for type assertions. It's more consistent across TypeScript and TSX files, and it's the modern recommended approach.
Non-null Assertion Operator
The non-null assertion operator (!) tells TypeScript that a value is not null or undefined:
// Function that might return null
function findUser(id: string): { name: string; email: string } | null {
// Simulate database lookup
if (id === '123') {
return { name: 'John', email: 'john@example.com' };
}
return null;
}
// Using non-null assertion
const user = findUser('123')!; // Assert that result is not null
console.log(user.name); // No error
// Without non-null assertion
const user2 = findUser('123');
// console.log(user2.name); // Error: Object is possibly 'null'
// Non-null assertion with optional chaining
interface Config {
apiKey?: string;
}
const config: Config = { apiKey: 'secret-123' };
// Assert that apiKey exists
const key: string = config.apiKey!;
console.log(key.toUpperCase());
// DOM element example
const button = document.getElementById('submit-btn')!;
button.addEventListener('click', () => {
console.log('Button clicked');
});
null or undefined, you'll get a runtime error. Use it only when you're absolutely certain the value exists.
Type Guards - typeof
The typeof operator is a built-in JavaScript operator that TypeScript uses for type narrowing:
// typeof with primitive types
function processValue(value: string | number): string {
if (typeof value === 'string') {
// TypeScript knows value is string here
return value.toUpperCase();
} else {
// TypeScript knows value is number here
return value.toFixed(2);
}
}
console.log(processValue('hello')); // "HELLO"
console.log(processValue(3.14159)); // "3.14"
// typeof with multiple types
function printValue(value: string | number | boolean | undefined): void {
if (typeof value === 'string') {
console.log(`String: ${value}`);
} else if (typeof value === 'number') {
console.log(`Number: ${value}`);
} else if (typeof value === 'boolean') {
console.log(`Boolean: ${value}`);
} else {
console.log('Value is undefined');
}
}
// typeof limitations
const arr = [1, 2, 3];
console.log(typeof arr); // "object" (not helpful for arrays)
const obj = { x: 10 };
console.log(typeof obj); // "object"
const nullValue = null;
console.log(typeof nullValue); // "object" (JavaScript quirk)
typeof works well for primitive types but has limitations with objects, arrays, and null. For these cases, use other type guards.
Type Guards - instanceof
The instanceof operator checks if an object is an instance of a specific class:
// Classes for demonstration
class Dog {
bark(): void {
console.log('Woof!');
}
}
class Cat {
meow(): void {
console.log('Meow!');
}
}
// Function using instanceof
function makeSound(animal: Dog | Cat): void {
if (animal instanceof Dog) {
// TypeScript knows animal is Dog here
animal.bark();
} else {
// TypeScript knows animal is Cat here
animal.meow();
}
}
const myDog = new Dog();
const myCat = new Cat();
makeSound(myDog); // "Woof!"
makeSound(myCat); // "Meow!"
// instanceof with built-in types
function processInput(input: Date | string): string {
if (input instanceof Date) {
// TypeScript knows input is Date here
return input.toISOString();
} else {
// TypeScript knows input is string here
return input.toUpperCase();
}
}
console.log(processInput(new Date()));
console.log(processInput('hello'));
// instanceof with Error handling
function handleError(error: Error | string): void {
if (error instanceof Error) {
console.error(`Error: ${error.message}`);
console.error(`Stack: ${error.stack}`);
} else {
console.error(`Error: ${error}`);
}
}
handleError(new Error('Something went wrong'));
handleError('Simple error message');
Type Guards - in Operator
The in operator checks if a property exists in an object:
// Types with different properties
type Fish = {
swim: () => void;
fins: number;
};
type Bird = {
fly: () => void;
wings: number;
};
// Function using 'in' operator
function move(animal: Fish | Bird): void {
if ('swim' in animal) {
// TypeScript knows animal is Fish here
console.log(`Swimming with ${animal.fins} fins`);
animal.swim();
} else {
// TypeScript knows animal is Bird here
console.log(`Flying with ${animal.wings} wings`);
animal.fly();
}
}
const fish: Fish = {
swim: () => console.log('Swimming...'),
fins: 4
};
const bird: Bird = {
fly: () => console.log('Flying...'),
wings: 2
};
move(fish); // "Swimming with 4 fins", "Swimming..."
move(bird); // "Flying with 2 wings", "Flying..."
// 'in' with optional properties
type UserProfile = {
name: string;
email: string;
phone?: string;
address?: {
city: string;
country: string;
};
};
function displayContact(profile: UserProfile): void {
console.log(`Name: ${profile.name}`);
console.log(`Email: ${profile.email}`);
if ('phone' in profile && profile.phone) {
console.log(`Phone: ${profile.phone}`);
}
if ('address' in profile && profile.address) {
console.log(`City: ${profile.address.city}`);
}
}
in operator is perfect for discriminating between object types based on their properties. It's especially useful when working with objects from external APIs.
Custom Type Guards (Type Predicates)
Custom type guards use type predicates (is keyword) to create reusable type-checking functions:
// Custom type guard function
function isString(value: unknown): value is string {
return typeof value === 'string';
}
// Usage
function processValue(value: unknown): void {
if (isString(value)) {
// TypeScript knows value is string here
console.log(value.toUpperCase());
} else {
console.log('Not a string');
}
}
// More complex type guard
interface User {
id: string;
name: string;
email: string;
}
interface Admin {
id: string;
name: string;
email: string;
permissions: string[];
}
// Type guard to check if User is Admin
function isAdmin(user: User | Admin): user is Admin {
return 'permissions' in user;
}
function grantAccess(user: User | Admin): void {
if (isAdmin(user)) {
// TypeScript knows user is Admin here
console.log(`Admin with ${user.permissions.length} permissions`);
} else {
// TypeScript knows user is User here
console.log(`Regular user: ${user.name}`);
}
}
// Array type guard
function isStringArray(value: unknown): value is string[] {
return (
Array.isArray(value) &&
value.every(item => typeof item === 'string')
);
}
function processArray(value: unknown): void {
if (isStringArray(value)) {
// TypeScript knows value is string[]
value.forEach(str => console.log(str.toUpperCase()));
} else {
console.log('Not a string array');
}
}
// Nullable type guard
function isDefined<T>(value: T | null | undefined): value is T {
return value !== null && value !== undefined;
}
// Filter array removing null/undefined
const values: (string | null | undefined)[] = ['a', null, 'b', undefined, 'c'];
const definedValues = values.filter(isDefined);
// Type is string[] (null and undefined removed)
console.log(definedValues); // ['a', 'b', 'c']
parameterName is Type in the return type. The function must return a boolean, and TypeScript will narrow the type accordingly.
Discriminated Unions with Type Guards
Type guards work exceptionally well with discriminated unions:
// Discriminated union
type SuccessResponse = {
status: 'success';
data: {
id: string;
name: string;
};
};
type ErrorResponse = {
status: 'error';
error: {
code: string;
message: string;
};
};
type ApiResponse = SuccessResponse | ErrorResponse;
// Type guard using discriminant
function isSuccessResponse(
response: ApiResponse
): response is SuccessResponse {
return response.status === 'success';
}
function isErrorResponse(
response: ApiResponse
): response is ErrorResponse {
return response.status === 'error';
}
// Using type guards
function handleResponse(response: ApiResponse): void {
if (isSuccessResponse(response)) {
// TypeScript knows response is SuccessResponse
console.log(`Success: ${response.data.name}`);
} else if (isErrorResponse(response)) {
// TypeScript knows response is ErrorResponse
console.error(`Error ${response.error.code}: ${response.error.message}`);
}
}
// Alternative: using discriminant directly in switch
function handleResponseSwitch(response: ApiResponse): void {
switch (response.status) {
case 'success':
console.log(`Success: ${response.data.name}`);
break;
case 'error':
console.error(`Error: ${response.error.message}`);
break;
}
}
Combining Type Guards
You can combine multiple type guards for complex type narrowing:
// Complex type structure
type User = {
type: 'user';
name: string;
email: string;
};
type Admin = {
type: 'admin';
name: string;
email: string;
permissions: string[];
};
type Guest = {
type: 'guest';
sessionId: string;
};
type Account = User | Admin | Guest;
// Multiple type guards
function hasEmail(account: Account): account is User | Admin {
return account.type === 'user' || account.type === 'admin';
}
function isAdmin(account: Account): account is Admin {
return account.type === 'admin';
}
// Using combined type guards
function processAccount(account: Account): void {
if (hasEmail(account)) {
// account is User | Admin (both have email)
console.log(`Email: ${account.email}`);
if (isAdmin(account)) {
// account is Admin
console.log(`Permissions: ${account.permissions.join(', ')}`);
}
} else {
// account is Guest
console.log(`Guest session: ${account.sessionId}`);
}
}
// Nested type narrowing
function getDisplayName(account: Account): string {
if ('name' in account) {
// account is User | Admin
return account.name;
} else {
// account is Guest
return `Guest (${account.sessionId})`;
}
}
Type Guards with Arrays
Type guards are useful for filtering and processing arrays:
// Mixed array type
type Item = string | number | boolean | null;
const items: Item[] = ['hello', 42, true, null, 'world', 0, false];
// Filter strings
const strings = items.filter((item): item is string => {
return typeof item === 'string';
});
console.log(strings); // ['hello', 'world']
// Filter numbers (excluding zero if needed)
const numbers = items.filter((item): item is number => {
return typeof item === 'number' && item !== 0;
});
console.log(numbers); // [42]
// Filter non-null values
const nonNullItems = items.filter((item): item is string | number | boolean => {
return item !== null;
});
console.log(nonNullItems); // All except null
// Complex object array
type Product = {
id: string;
name: string;
price: number;
discount?: number;
};
const products: Product[] = [
{ id: '1', name: 'Laptop', price: 999, discount: 0.1 },
{ id: '2', name: 'Mouse', price: 29 },
{ id: '3', name: 'Keyboard', price: 79, discount: 0.15 }
];
// Filter products with discounts
function hasDiscount(product: Product): product is Product & { discount: number } {
return product.discount !== undefined && product.discount > 0;
}
const discountedProducts = products.filter(hasDiscount);
discountedProducts.forEach(product => {
// TypeScript knows discount is defined
console.log(`${product.name}: ${product.discount * 100}% off`);
});
- Create a discriminated union
Shapewith three types:Circle(kind: 'circle', radius: number),Rectangle(kind: 'rectangle', width: number, height: number), andSquare(kind: 'square', size: number) - Create custom type guard functions
isCircle,isRectangle, andisSquarefor each shape type - Create a function
calculateAreathat uses type guards to calculate the area of any shape - Create a function
isLargeShapethat returns true if the shape's area is greater than 100 - Test your functions with an array of mixed shapes and filter the large ones
Summary
- Type assertions use
askeyword to tell TypeScript about a value's type - Non-null assertion (
!) asserts a value is not null or undefined - typeof type guard checks primitive types at runtime
- instanceof type guard checks if an object is an instance of a class
- in operator type guard checks if a property exists in an object
- Custom type guards use type predicates (
is) for reusable type checks - Type guards enable safe type narrowing based on runtime checks
- Combine type guards for complex type narrowing scenarios
- Type assertions bypass checks; type guards provide safe narrowing