Skip to main content

Assertions

AssertTrue, AssertFalse, Equal, and how they surface failures.

The assertion API is intentionally tiny. Two assertion types and one strict-equality check cover most type-level testing.

AssertTrue

import type { AssertTrue } from '@gentleduck/ttest/assert'

AssertTrue<T extends true, Msg extends string> resolves to true when T is true. When T is not exactly true, the constraint T extends true fails and the compiler reports the assertion line — your Msg string lives there as a constant reminder of what was being checked.

type pass = AssertTrue<true, 'always passes'>
type fail = AssertTrue<false, 'never passes'>
//          ^ TS2344: Type 'false' does not satisfy the constraint 'true'.

AssertFalse

import type { AssertFalse } from '@gentleduck/ttest/assert'

AssertFalse<T extends false, Msg extends string> is the negative-polarity twin.

type ok = AssertFalse<false, 'must be false'>
type no = AssertFalse<true, 'must be false'>
//        ^ TS2344: Type 'true' does not satisfy the constraint 'false'.

Use it when "this is not equal" or "this is not assignable" is the assertion you want to make explicit, instead of negating inside the predicate.

Equal

import type { Equal } from '@gentleduck/ttest/equality'

Equal<X, Y> returns true if X and Y are bidirectionally and variance-preservingly equal. It does so by comparing two type-functions:

type Equal<X, Y> =
  (<T>() => T extends X ? 1 : 2) extends (<T>() => T extends Y ? 1 : 2) ? true : false

This is the only equality check in the type system that reliably distinguishes:

  • any from unknown (a structural X extends Y && Y extends X would say they're equal)
  • string from string & {} (which Equal treats as different — important when authoring LiteralUnion)
  • mutable from readonly tuples
import type { AssertTrue } from '@gentleduck/ttest/assert'
import type { Equal } from '@gentleduck/ttest/equality'

type t1 = AssertTrue<Equal<{ a: 1 }, { a: 1 }>, 'identical shapes equal'>
type t2 = AssertTrue<Equal<any, unknown>, 'any equals unknown'>
//        ^ FAILS: Equal returns false here — that is the point.

NotEqual

import type { NotEqual } from '@gentleduck/ttest/equality'

type t = AssertTrue<NotEqual<1, 2>, '1 must not equal 2'>

Inverse of Equal. Useful when negating reads cleaner than wrapping in AssertFalse.

Extends / NotExtends

duck-ttest does not ship dedicated Extends / NotExtends aliases — write the assertion directly:

import type { AssertTrue, AssertFalse } from '@gentleduck/ttest/assert'

// "X extends Y" — assignability
type _ext  = AssertTrue<'admin' extends 'admin' | 'user' ? true : false, 'admin is a role'>

// "X does not extend Y"
type _next = AssertFalse<string extends 'admin' ? true : false, 'string is not the literal admin'>

For union-position assertions, wrap with [X] extends [Y] to disable distribution — see Composing tests / Distribution traps.

ShallowEqual / Extends-style checks

import type { ShallowEqual } from '@gentleduck/ttest/equality'

ShallowEqual<X, Y> is one-way assignability: X extends Y ? true : false. Use this when you want to assert "X is assignable to Y" without caring about the reverse direction.

type t = AssertTrue<ShallowEqual<'admin', 'admin' | 'user'>, 'admin is in union'>

For richer conditional branching use IfExtends from @gentleduck/ttest/conditional:

import type { IfExtends } from '@gentleduck/ttest/conditional'

type Trim<S extends string> = IfExtends<S, ` ${infer R}`, Trim<R>, S>

Picking the right assertion

You want to checkUse
X equals Y exactly (variance-preserving)AssertTrue<Equal<X, Y>, Msg>
X does not equal YAssertTrue<NotEqual<X, Y>, Msg> or AssertFalse<Equal<X, Y>, Msg>
X is assignable to YAssertTrue<ShallowEqual<X, Y>, Msg>
X is not assignable to YAssertFalse<ShallowEqual<X, Y>, Msg>
Custom predicate P<X> is trueAssertTrue<P<X>, Msg>

Common failure recipes

A short catalog of what each error message usually means.

Compiler errorWhat brokeFix
TS2344: Type 'false' does not satisfy 'true'AssertTrue<Predicate, ...> failed the inner predicate is false.Print the predicate's value (hover, or extract to a named type) and inspect both sides.
TS2344: Type 'true' does not satisfy 'false'AssertFalse<Predicate, ...> failed.Same shape, opposite polarity.
Type 'boolean' does not satisfy 'true'Predicate returned true | false. Usually distribution over any, boolean, or a wide union.Wrap in [T] extends [U] or guard with IsAny first.
Type instantiation is excessively deepRecursive helper hit the type-checker's depth cap.Cap recursion depth (Paths<T, 4>), or split the test into smaller files.
Hover shows true but the test failsStale build artifact.Restart the TS server / rm -rf node_modules/.cache.

Grouping assertions

Tuples are the smallest unit of grouping — TypeScript will report the index along with the message:

type tests = [
  AssertTrue<Equal<1, 1>, 'one'>,
  AssertTrue<Equal<2, 2>, 'two'>,
  AssertFalse<Equal<1, 2>, 'one is not two'>,
]

For larger suites, see Core / Composing tests.