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:
anyfromunknown(a structuralX extends Y && Y extends Xwould say they're equal)stringfromstring & {}(whichEqualtreats as different — important when authoringLiteralUnion)- mutable from
readonlytuples
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 check | Use |
|---|---|
| X equals Y exactly (variance-preserving) | AssertTrue<Equal<X, Y>, Msg> |
| X does not equal Y | AssertTrue<NotEqual<X, Y>, Msg> or AssertFalse<Equal<X, Y>, Msg> |
| X is assignable to Y | AssertTrue<ShallowEqual<X, Y>, Msg> |
| X is not assignable to Y | AssertFalse<ShallowEqual<X, Y>, Msg> |
Custom predicate P<X> is true | AssertTrue<P<X>, Msg> |
Common failure recipes
A short catalog of what each error message usually means.
| Compiler error | What broke | Fix |
|---|---|---|
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 deep | Recursive 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 fails | Stale 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.