Type Testing Utilities
Canon provides zero-runtime-cost helpers for asserting type relationships at compile time. Use them to guard against regressions and to document expectations directly in your code.
Quick Start
ts
import type { Expect, IsFalse, IsTrue } from '@relational-fabric/canon'
import { invariant } from '@relational-fabric/canon'
interface User {
id: string
role: 'admin' | 'user'
}
// Positive assertion – `id` must be a string
void invariant<Expect<User['id'], string>>()
// Negative assertion – `role` should not be a number
void invariant<IsFalse<Expect<User['role'], number>>>()
// Exact match in both directions
type CanonicalRole = 'admin' | 'user'
void invariant<Expect<User['role'], CanonicalRole>>()
void invariant<Expect<CanonicalRole, User['role']>>()API Reference
Expect<A, B>– resolves totruewhenAextendsB, otherwisefalse.IsTrue<A>– shorthand forExpect<A, true>.IsFalse<A>– resolves totruewhenAis exactlyfalse.invariant<T extends true>()– runtime no-op that fails to compile ifTis nottrue.
Best Practices
- Prefer positive assertions (
Expect<T, Expected>) and accompany negative checks with@ts-expect-errorcomments. - Co-locate invariants near the types they verify so the intent stays visible during reviews.
- Prefix invariants with
voidwhen you do not need the return value – this keeps ESLint happy in expression contexts. - Combine invariants with helper aliases such as
type Assert<T extends true> = Twhen you need to express complex relationships without introducing runtime statements.
Self-Validation Patterns
ts
import type { Expect, IsFalse, IsTrue } from '@relational-fabric/canon'
import { invariant } from '@relational-fabric/canon'
void invariant<Expect<true, true>>()
void invariant<Expect<'value', string>>()
void invariant<IsFalse<Expect<string, number>>>()
// @ts-expect-error - Expect should fail when the left side does not extend the right.
void invariant<Expect<{ id: string }, { id: number }>>()
// @ts-expect-error - IsTrue rejects non-true values.
void invariant<IsTrue<false>>()
// @ts-expect-error - IsFalse only accepts precisely `false`.
void invariant<IsFalse<true>>()Integration Tips
- IDE Feedback – failed invariants surface as regular TypeScript errors. Jump to definition from the error to inspect the assertion.
- CI/CD – invariants run automatically when you execute
npm run check:types(tsc --noEmit). No additional tooling is required. - Examples – use the utilities inside example files with
void invariant<Expect<...>>()to keep lint checks satisfied.
Advanced Usage
Domain-Specific Helpers
ts
import type { Expect } from '@relational-fabric/canon'
import { invariant } from '@relational-fabric/canon'
type ElementType<T> = T extends Array<infer E> ? E : never
void invariant<Expect<ElementType<string[]>, string>>()
void invariant<Expect<ElementType<Array<{ id: string }>>, { id: string }>>()Conditional Assertions
ts
import type { Expect } from '@relational-fabric/canon'
import { invariant } from '@relational-fabric/canon'
type NonEmptyArray<T> = T extends [unknown, ...unknown[]] ? true : false
void invariant<Expect<NonEmptyArray<[number]>, true>>()
void invariant<Expect<NonEmptyArray<[]>, false>>()Error Message Clarity
Name intermediate types before asserting them so that TypeScript produces clean diagnostics:
ts
type EmailAddress = User['email']
// @ts-expect-error - Emails are strings, not numbers
void invariant<Expect<EmailAddress, number>>()These utilities are lightweight enough to use directly in production code, tests, and examples. Because they erase at runtime, they keep bundles lean while still documenting intent.