Assertion Functions (asserts)
Assertion functions are a TypeScript feature that narrows types by throwing an error
if a condition is not met. Unlike type predicates (which return true/false),
assertion functions either throw or do nothing — and TypeScript narrows the type
after the call because the function is guaranteed not to return if the condition fails.
The return type is written as asserts condition or asserts param is Type.
The Two Forms of Assertion Functions
Form | Syntax | When to use |
|---|---|---|
Assert truthy | asserts condition | Assert any expression is truthy |
Assert type | asserts param is Type | Assert a parameter is a specific type |
Form 1: asserts condition
The simplest form asserts that a condition is truthy. TypeScript narrowing the types of all variables involved in the expression after the call.
function assert(condition: unknown, message?: string): asserts condition {
if (!condition) {
throw new Error(message ?? 'Assertion failed')
}
}
function processUser(user: User | null) {
assert(user !== null, 'User must not be null')
// After this line, TypeScript knows: user is User (not null)
console.log(user.name) // safe — no optional chaining needed
console.log(user.email.toLowerCase()) // also safe
}
interface User {
name: string
email: string
}assert throws if the condition is false, any code that runs after the call is guaranteed to be in the "condition was true" branch. TypeScript understands this and narrows accordingly.Form 2: asserts param is Type
The second form asserts that a specific parameter is a certain type after the function runs. This is the assertion equivalent of a type predicate.
function assertIsString(val: unknown): asserts val is string {
if (typeof val !== 'string') {
throw new TypeError(`Expected string, got ${typeof val}`)
}
}
function assertIsNumber(val: unknown): asserts val is number {
if (typeof val !== 'number') {
throw new TypeError(`Expected number, got ${typeof val}`)
}
}
function processValue(val: unknown) {
assertIsString(val)
// TypeScript knows: val is string here
console.log(val.toUpperCase()) // safe!
}
// Parsing raw config data
function parseConfig(data: unknown) {
const config = data as Record<string, unknown>
assertIsString(config.host)
assertIsNumber(config.port)
// After assertions, both are narrowed:
return { host: config.host, port: config.port }
// host: string, port: number
}Assertion Functions vs Type Predicates
Both assertion functions and type predicates narrow types, but they're used differently:
Feature | Type Predicate (is) | Assertion Function (asserts) |
|---|---|---|
Returns | boolean (true/false) | void (or throws) |
Narrowing happens | Inside if branch | After the call |
On failure | Returns false | Throws an error |
Use in conditions | Yes: if (isUser(x)) | No: cannot use in if condition |
Use in .filter() | Yes | No |
Best for | Conditional branching | Invariant checking, preconditions |
Real-World: Validating Environment Variables
A perfect use case for assertion functions: ensuring required environment variables exist before your app starts. The assertion runs at startup and throws immediately if something is missing, rather than failing mysteriously later.
function assertEnvVar(name: string): asserts name is string {
// This just narrows — but the real check is below
void name
}
function requireEnv(name: string): string {
const value = process.env[name]
if (!value) {
throw new Error(`Missing required environment variable: ${name}`)
}
return value
}
// Validate all required env vars at startup
const config = {
databaseUrl: requireEnv('DATABASE_URL'),
apiKey: requireEnv('API_KEY'),
port: parseInt(requireEnv('PORT'), 10),
}
// From this point on, all values are strings (not undefined)
console.log(`Connecting to ${config.databaseUrl}`)Real-World: DOM Element Assertions
function assertElement<T extends Element>(
selector: string,
type: new (...args: any[]) => T,
): asserts type is new (...args: any[]) => T {
void type // placeholder
}
// Simpler, more practical version:
function getElement<T extends Element>(
selector: string,
ElementType: new (...args: any[]) => T,
): T {
const el = document.querySelector(selector)
if (!(el instanceof ElementType)) {
throw new Error(`Element "${selector}" not found or wrong type`)
}
return el
}
// Usage:
const form = getElement('#login-form', HTMLFormElement)
// form is HTMLFormElement — not HTMLElement | null
form.addEventListener('submit', (e) => {
e.preventDefault()
const data = new FormData(form) // TypeScript knows it's HTMLFormElement
})Assertion Functions in Class Constructors
Assertion functions are useful for validating constructor arguments, throwing immediately with a clear error message rather than silently creating broken objects.
function assertPositive(value: number, name: string): asserts value is number {
if (value <= 0) {
throw new RangeError(`${name} must be positive, got ${value}`)
}
}
function assertNonEmpty(str: string, name: string): asserts str is string {
if (str.trim().length === 0) {
throw new Error(`${name} must not be empty`)
}
}
class Product {
readonly id: string
readonly name: string
readonly price: number
constructor(id: string, name: string, price: number) {
assertNonEmpty(id, 'Product ID')
assertNonEmpty(name, 'Product name')
assertPositive(price, 'Product price')
this.id = id
this.name = name
this.price = price
}
}
// Throws: "Product price must be positive, got -5"
// new Product('abc', 'Widget', -5)Never-Reaching Assertion
A special assertion function that always throws is useful for exhaustiveness checking. If TypeScript determines a code path is unreachable, it should never be called.
function assertNever(value: never, message?: string): never {
throw new Error(message ?? `Unexpected value: ${JSON.stringify(value)}`)
}
type Shape = { kind: 'circle'; radius: number } | { kind: 'square'; side: number }
function area(shape: Shape): number {
switch (shape.kind) {
case 'circle':
return Math.PI * shape.radius ** 2
case 'square':
return shape.side ** 2
default:
// If you add a new Shape variant and forget to handle it,
// TypeScript errors HERE at compile time
return assertNever(shape)
}
}asserts return types. Without the annotation, the function is treated as returning void and the narrowing won't work.Combining Assertion with Zod or Similar
// Using Zod for runtime validation with assertion-style API
// (Zod's .parse() throws on invalid — same semantic as assertions)
// Pure TypeScript equivalent without Zod:
interface ApiResponse {
users: { id: number; name: string }[]
total: number
}
function assertIsApiResponse(data: unknown): asserts data is ApiResponse {
if (
typeof data !== 'object' ||
data === null ||
!Array.isArray((data as any).users) ||
typeof (data as any).total !== 'number'
) {
throw new Error('Invalid API response shape')
}
}
async function getUsers(): Promise<ApiResponse> {
const res = await fetch('/api/users')
const data: unknown = await res.json()
assertIsApiResponse(data)
// data is ApiResponse from here on
return data
}