Introduction
A TypeScript type-level test framework and a large library of type utilities. Zero runtime cost.
What is duck-ttest?
@gentleduck/ttest is two things in one package:
- A small, focused type-level test framework —
AssertTrue,AssertFalse,Equal, and friends. When a test fails, the compiler surfaces a readable message at the failure site. - A library of type utilities organized into 37 narrow modules —
any,aria,assert,async,bit,boolean,brand,class,conditional,css,date,discriminated,emitter,equality,extraction,format,fp,fs,function,geometry,guard,http,json,literal,locale,number,object,pattern,predicates,primitive,promise,result,router,sql,template,tuple,union.
Each utility is a type, so the entire package is erased at compile time. There is no runtime, no bundle bytes, and nothing to ship.
Why duck-ttest?
The TypeScript ecosystem already has tsd, expect-type, and type-fest. duck-ttest covers ground each of them touches separately:
tsdruns assertions through a CLI on top oftsc. duck-ttest assertions are types that fail your normal type-check — no extra runner needed.expect-typeis fluent and ergonomic, but stops at the test framework boundary. duck-ttest ships the framework and the utilities that the framework asserts against.type-festis a beloved utility library, but has no test side. duck-ttest gives you both: write a utility and assert it the same way the test suite does.
If you build schema generators, ORMs, validation libraries, or type-heavy SDKs, you typically need: a way to declare invariants, a way to check them, and a way to ship them with zero runtime cost. That is what duck-ttest is.
Features
| Feature | Description |
|---|---|
| Compile-time only | package.json ships src/ only. Every export is a type — nothing reaches the JS output of consumers. |
| 37 utility modules | Sub-path imports such as @gentleduck/ttest/object, @gentleduck/ttest/sql, @gentleduck/ttest/router. Tree-shake by importing only what you need. |
| Assertions | AssertTrue<T, Msg> and AssertFalse<T, Msg> produce a typed error containing Msg when the assertion fails. |
| Strict equality | Equal<X, Y> uses the conditional-distribution trick to distinguish any from unknown and preserve variance — narrower than X extends Y && Y extends X. |
| Branded types | Brand<T, 'UserId'> and Unbrand<T> for nominal typing without runtime tagging. |
| SQL schema inference | InferSchema<'CREATE TABLE ...'> parses DDL into a typed row shape. |
| Higher-kinded types | Fn / Apply / MapTuple / Reduce / ComposeFn — first-class type-level functions. |
| Domain unions | Curated ARIA roles, CSS keywords, HTTP statuses, ISO locales — tiny, accurate, with no string widening unless you ask for it. |
| Result / Option | Rust-style Result<T, E> and Option<T> as type-level discriminated unions. |
| Router patterns | PathParams<'/users/:id'> → { id: string }. BuildPath, MatchRoute, QueryParams. |
| Glob matching | MatchesGlob<S, P> with *, ?, and ** support. |
A 30-second tour
import type { AssertTrue, Equal } from '@gentleduck/ttest/assert'
import type { IsAny, IsNever } from '@gentleduck/ttest/any'
import type { InferSchema } from '@gentleduck/ttest/sql'
// 1. Test a type the same way you'd test a function.
type t1 = AssertTrue<Equal<IsAny<unknown>, false>, 'unknown is not any'>
type t2 = AssertTrue<Equal<IsNever<never>, true>, 'never should be detected'>
// 2. Use the utilities directly in your own types.
type User = InferSchema<'CREATE TABLE users (id INT PRIMARY KEY, email TEXT NOT NULL)'>
// ^? { id?: number; email: string }
If Equal<IsAny<unknown>, false> ever became false, the message 'unknown is not any' would surface as a Type 'string' is not assignable to type 'never' error pointing at t1.
Architecture
There is no runtime layer in this diagram. The tsc step is the runner.
Documentation map
| Section | What it covers |
|---|---|
| Installation | Install the package, configure tsconfig, and import a sub-path. |
| Getting started | Five-minute tutorial: write a failing test, see the error, fix it. |
| Comparison | Honest tradeoffs versus tsd, expect-type, and type-fest. |
| Migration | v0.x → v1.0 breaking renames. |
| Core — Assertions | AssertTrue, AssertFalse, Equal, Extends. |
| Core — Type-level testing | Workflow with tsc, vitest @ts-expect-error, and tsd. |
| Core — Composing tests | Naming, organization, and pitfalls. |
| API reference | Every exported type across all 35 modules. |
| Guides | Branded types, discriminated unions, schema builders, regression suites. |
Philosophy
In a schema generator, ORM, or framework, the type is the contract. duck-ttest closes the loop between authoring a contract and verifying it — at the same compile step that catches every other type error.