What Are Assertion Functions? 💡
An assertion function is a function that throws an error if a given condition is not met. It helps TypeScript refine the type of a value dynamically within a scope.
Syntax
function assertFunction(value: unknown): asserts value is Type {
if (!someCondition(value)) {
throw new Error("Invalid type");
}
}The assert function itself does not return anything, so instead of a return type, we use asserts value is Type. If the function does not throw an error, TypeScript can assume that value is of the specified type further in the code execution.
Example: Basic Assertion Function
function assertString(value: unknown): asserts value is string {
if (typeof value !== "string") {
throw new Error("Expected a string");
}
}function greet(name: unknown) {
assertString(name);
console.log(name.toUpperCase()); // TypeScript knows name is a string here
}
greet("John"); // JOHN
greet(42); // Will throw errors
// Output:
// JOHN
// Executed JavaScript Failed.
// Expected a string
Here, assertStringtakes an unknown value and checks if the value is a string. If the value is not a string, it will throw an error. The asserts value is string return type to tell TypeScript that if the function does not throw, value must be a string.
After executing through assertString, TypeScript understands that the value is guaranteed to be a string.
Why Not Just Use typeof Check? 🤔
While the typeof check ensures at runtime that value is a string (or throws an error), TypeScript's static type system does not infer that on its own. Without asserts value is string, TypeScript does not narrow the type of value after the function call.
Example: Without asserts value is string
function assertString(value: unknown) {
if (typeof value !== "string") {
throw new Error("Expected a string");
}
}function greet(name: unknown) {
assertString(name);
console.log(name.toUpperCase()); // ERROR: 'name' is of type 'unknown'
}
greet("John");
Even though assertString ensures at runtime that name is a string, TypeScript still sees name as unknown after calling assertString. It does not automatically refine the type.
Example: With asserts value is string
function assertString(value: unknown): asserts value is string {
if (typeof value !== "string") {
throw new Error("Expected a string");
}
}function greet(name: unknown) {
assertString(name);
console.log(name.toUpperCase()); // No TypeScript error, 'name' is now a string
}
greet("John");
// Output:
// JOHN
Here, asserts value is string tells TypeScript “If assertString runs successfully (without throwing an error), then value must be a string.” Thus, TypeScript automatically narrows the type of name to string after calling assertString.
Practical Examples Using Assertion Functions🛠️
Let’s explore some real-world examples where assertion functions are beneficial.
Example 1: Asserting an Object Structure
Here we see an example where we want to ensure that an unknownobject is compatible with the Userinterface.
interface User {
id: number;
name: string;
}function assertUser(value: unknown): asserts value is User {
if (typeof value !== "object" || value === null) {
throw new Error("Not a valid user object");
}
if (!("id" in value) || typeof (value as any).id !== "number") {
throw new Error("User must have a numeric id");
}
if (!("name" in value) || typeof (value as any).name !== "string") {
throw new Error("User must have a string name");
}
}
const data: unknown = { id: 1, name: "John Doe" };
assertUser(data); // If this does not throw, TypeScript knows `data` is a User
console.log(data.id); // Safe access
console.log(data.name);
// Output:
// 1
// John Doe
This can be very useful when validating API responses, as APIs often return unknown data.
async function fetchUser(id: number): Promise<User> {
const response = await fetch(`https://api.example.com/users/${id}`);
const data = await response.json();
assertUser(data);
return data; // TypeScript now knows `data` is a User
}Example 2: Ensuring Non-Empty Strings in User Input
User inputs often need validation before being processed. Here we ensure that user input is a valid string before performing operations on it.
function assertNonEmptyString(value: unknown): asserts value is string {
if (typeof value !== "string" || value.trim() === "") {
throw new Error("Expected a non-empty string");
}
}const userInput: unknown = process.argv[2];
assertNonEmptyString(userInput);
console.log(userInput.toUpperCase()); // Safe as TypeScript knows userInput is a string
Example 3: Ensuring a Value is Not Null or Undefined
When working with optional values, assertion functions ensure they are present before proceeding.
function assertNotNull<T>(value: T | null | undefined): asserts value is T {
if (value == null || value == undefined) {
throw new Error("Value cannot be null or undefined");
}
}const config = { apiKey: "test" } as { apiKey: string | null };
assertNotNull(config.apiKey);
console.log(config.apiKey); // No error
// Output:
// test
The assertNotNull function verifies that config.apiKey is not null or undefined, throwing an error otherwise. Since "test" is a valid string, TypeScript refines its type, enabling safe access to config.apiKey, which prints "test".
Example 4: Using Assertions in Type Guards
This example showcases how assertion functions provide runtime safety and TypeScript type narrowing, ensuring only valid data is used after calling assertUserGuard.
interface User {
id: number;
name: string;
}function isUser(value: unknown): value is User {
return typeof value === "object" && value !== null && "id" in value && "name" in value;
}
function assertUserGuard(value: unknown): asserts value is User {
if (!isUser(value)) {
throw new Error("Invalid User");
}
}
const data: unknown = { id: 123, name: 'John'}
assertUserGuard(data);
console.log(data.name);
// Output:
// John
The assertUserGuard function checks if data is a valid User, throwing an error if it's not. Since { id: 123, name: 'John' } meets the User structure, TypeScript treats data as a User, enabling safe access to data.name, which prints "John".
Example 5: Using Assertions Combined with Generics
This code demonstrates how to use assertion functions in TypeScript to validate both individual objects and arrays of a specific type at runtime. It ensures that an unknown value is a valid User or an array of User objects before accessing their properties.
interface User {
id: number;
name: string;
}function isUser(value: unknown): value is User {
return typeof value === "object" && value !== null && "id" in value && "name" in value;
}
function assertArray<T>(value: unknown, assertItem: (item: unknown) => asserts item is T): asserts value is T[] {
if (!Array.isArray(value)) {
throw new Error("Expected an array");
}
value.forEach(assertItem);
}
function assertUserGuard(value: unknown): asserts value is User {
if (!isUser(value)) {
throw new Error("Invalid User");
}
console.log("Is user!");
}
const data: unknown = [{ id: "1", name: "Jane" }, { id: "2", name: "John" }];
assertArray(data, assertUserGuard);
console.log(data[0].name); // Safe operation
// Output:
// Is user!
// Is user!
// Jane
The assertUserGuard function checks if a value is a User, throwing an error if it's not. The assertArray function applies an assertion function to each item in an array, ensuring all elements meet the expected type. When called on data, it verifies each object, prints "Is user!", and allows safe property access without extra type checks.
Conclusion 📣
In this article, we explored assertion functions, which provides a robust way to enforce type safety at runtime while improving type inference. By explicitly verifying conditions and refining types, they help eliminate unnecessary type assertions and prevent runtime errors. Whether validating API responses, ensuring non-null values, or enforcing object structures, assertion functions make TypeScript code more predictable, maintainable, and error-resistant.
Through real-world examples, we’ve seen how assertion functions can enhance the safety of operations, prevent errors, and make your code more predictable and maintainable. By leveraging asserts value is Type, TypeScript can infer and narrow types automatically, reducing the need for manual type casting and helping developers avoid common pitfalls in dynamic or uncertain code.