Ts.Assert
TypeScript Test Module
Type-level and value-level assertion utilities for testing TypeScript code.
Core Concepts
Two Testing Modes
This module supports two distinct testing modes:
TVA Mode (Type-Value-Arg)
Provide expected type, then pass a value to check:
// Single call with value
Ts.Assert.exact<string>()('hello')
// Preserve literal types with .const()
Ts.Assert.exact<1>().const(1)Signature: <Expected>() => (value) => void
TTA Mode (Type-Type-Arg)
Provide both expected and actual types:
// Type-only check - no value
Ts.Assert.exact<string, string>()
// Fails - requires error parameter
Ts.Assert.exact<string, number>() // ❌ Expected 1 arguments, but got 0Signature: <Expected, Actual>() => void (passes) or <Expected, Actual>(error) => void (fails)
Error Feedback
When assertions fail, errors appear in parameter types:
// TVA mode - error shows in value parameter type
Ts.Assert.exact<string>()(42)
// Parameter type becomes: { ERROR: "...", expected: string, actual: number }
// TTA mode - error parameter required
Ts.Assert.exact<string, number>()
// Must provide: (error: { ERROR: "...", expected: string, actual: number })Unary Relators
Relators that check properties of a single type (no comparison). Available at top-level and directly executable.
any / unknown / never
Check if type is an edge case:
Assert.any(x as any) // ✅
Assert.unknown(x as unknown) // ✅
Assert.never(x as never) // ✅
// With negation
Assert.not.any(x as string) // ✅
// Note: Also available under binary relators for backward compat
Assert.exact.any(x as any) // ✅ (same behavior)empty
Assert type is empty:
Assert.empty([]) // ✅ empty array
Assert.empty('' as const) // ✅ empty string
Assert.empty({} as Record<string, never>) // ✅ empty object
// With negation
Assert.not.empty([1]) // ✅ not empty
// Common mistakes
Assert.empty({}) // ❌ {} = non-nullish!Important: {} means "non-nullish", not empty. Use Record<PropertyKey, never>.
Binary Relators
Binary relators compare two types (expected vs actual).
exact
Types must be exactly equal (mutual subtype):
exact<A, B> // A extends B AND B extends Asub
Actual must be subtype of expected:
sub<A, B> // B extends Aequiv
Types are equivalent (ignoring excess properties):
equiv<A, B> // A and B have same structure, allow excessnot.*
Negated relations:
not.exact<A, B> // NOT (A extends B AND B extends A)
not.sub<A, B> // NOT (B extends A)
not.equiv<A, B> // NOT (A equivalent to B)Extractors
awaited
Extract and assert on promised type:
awaited.is<number>()(Promise.resolve(1)) // OK
awaited.is<string>()(Promise.resolve(1)) // ❌returned
Extract and assert on return type:
returned.is<number>()(() => 1) // OK
returned.awaited<string>()(() => Promise.resolve('x')) // OKparameter
Assert on function parameter type:
parameter<string>()((x: string) => {}) // OK
parameter2<number>()((a: string, b: number) => {}) // OK (2nd param)
parameters<[string, number]>()((x: string, y: number) => {}) // OK (all)Special Types (Legacy - see Unary Relators above)
These matchers are available under binary relators for backward compatibility, but work identically to their top-level unary relator counterparts.
Collection Types
array
Assert on array element type:
array<string>()(['a', 'b']) // OKtuple
Assert exact tuple type:
tuple<[string, number]>()(['a', 1]) // OKindexed
Extract and assert on index signature value type:
// For types with string index signatures
A.exact.indexed<number, Record<string, number>> // OK - value type is number
A.exact.indexed<Foo, { [k: string]: Foo }> // OK - value type is Foo
// Error: no index signature
A.exact.indexed<string, { name: string }> // Error - no index signatureProperties
properties
Assert object has required properties (allows excess):
properties<{ id: string }>()({ id: '1', name: 'test' }) // OKType-Level Testing
For batch type assertions in test files:
// Single assertion - must extend never (pass)
type _ = Ts.Assert.Case<Equal<string, string>> // OK
// Multiple assertions
type _ = Ts.Assert.Cases<
Equal<string, string>, // ✓
Sub<'hello', string>, // ✓
Exact<number, number> // ✓
>Technical Details
Literal Type Preservation
Scalars with as const preserve their literal types when passed to generics:
const x = 1 as const
type T = typeof x // 1 (not number)
Ts.Assert.exact<1>()(x) // OK - x is type 1Use .const() method for explicit literal preservation:
Ts.Assert.exact<1>().const(1) // Preserves literal 1Never Detection
Passing never to non-never assertions requires explicit error parameter:
Ts.Assert.exact<number>()(null as never)
// ❌ Requires: (error: NeverNotAllowedError)
Ts.Assert.exact<never>()(null as never) // OKError Messages
Error objects use aligned keys for readability:
{
ERROR_________: 'Types are not exactly equal'
expected______: string
actual________: number
}Key length configured via KitLibrarySettings.Ts.Assert.errorKeyLength.
/**
* Type-level assertion utilities for testing type correctness.
*
* ## The Chaining API
*
* All assertions follow a consistent, compositional pattern:
*
* ```typescript
* Ts.Assert[.not].<relation>.<extractor?>.<extractor?>...<TypeParams>
*
```
*
* Where:
* - **Relation**: `exact` (structural equality), `equiv` (mutual assignability), `sub` (subtype)
* - **Extractor**: Optional transformation (`.awaited`, `.returned`, `.parameter`, etc.)
* - **Negation**: Optional `.not` prefix negates the assertion
*
* ## Quick Examples
*
* ```typescript
* // Type Level
* type _ = Ts.Assert.Cases <
* Ts.Assert.exact<string, string>, // Plain relation
* Ts.Assert.sub.awaited<User, Promise<AdminUser>>, // With extractor
* Ts.Assert.exact.returned.awaited<Data, AsyncFn>, // Chained extractors
* Ts.Assert.not.equiv<string, number> // Negation
* >
*
* // Value Level (requires .is for identity)
* Ts.Assert.exact.of.as<string>()(value)
* Ts.Assert.sub.awaited<number>()(promise)
* Ts.Assert.exact.returned.awaited<User>()(asyncFn)
* Ts.Assert.not.sub.of.as<number>()(value)
*
```
*
* ## Relations
*
* ### `exact` - Structural Equality
* Types must be structurally identical. Most strict assertion.
*
* ```typescript
* type T = Ts.Assert.exact<string, string> // ✓ Pass
* type T = Ts.Assert.exact<1 | 2, 2 | 1> // ✗ Fail (different structure)
* type T = Ts.Assert.exact<string & {}, string> // ✗ Fail (different structure)
*
```
*
* ### `equiv` - Mutual Assignability (Semantic Equality)
* Types must be mutually assignable (compute to the same result).
*
* ```typescript
* type T = Ts.Assert.equiv<1 | 2, 2 | 1> // ✓ Pass (same computed type)
* type T = Ts.Assert.equiv<string & {}, string> // ✓ Pass (both compute to string)
* type T = Ts.Assert.equiv<string, number> // ✗ Fail (not mutually assignable)
*
```
*
* ### `sub` - Subtype Checking
* Actual must extend Expected. Most commonly used relation.
*
* ```typescript
* type T = Ts.Assert.sub<string, 'hello'> // ✓ Pass ('hello' extends string)
* type T = Ts.Assert.sub<object, { a: 1 }> // ✓ Pass (more specific extends less)
* type T = Ts.Assert.sub<'hello', string> // ✗ Fail (string doesn't extend 'hello')
*
```
*
* ## Extractors
*
* Extractors transform types before applying the relation check.
*
* ### Special Types
* - `.Never<T>` / `.never()` - Check if type is `never` (type-level uses PascalCase due to keyword)
* - `.Any<T>` / `.any()` - Check if type is `any`
* - `.Unknown<T>` / `.unknown()` - Check if type is `unknown`
* - `.empty<T>` - Check if type is empty ([], '', or empty object)
*
* ```typescript
* type T = Ts.Assert.equiv.Never<never> // ✓ Pass
* Ts.Assert.exact.any()(value) // Value level (lowercase)
*
```
*
* ### Containers
* - `.array<Element, T>` - Check array element type
* - `.tuple<[...], T>` - Check tuple structure
* - `.indexed<Element, T>` - Extract value type from index signature
*
* ```typescript
* type T = Ts.Assert.sub.array<number, (1 | 2 | 3)[]> // ✓ Pass
* type T = Ts.Assert.exact.indexed<number, Record<string, number>> // ✓ Pass
*
```
*
* ### Transformations (Chainable)
* - `.awaited` - Extract resolved type from Promise
* - `.returned` - Extract return type from function
*
* **These are namespace-only** (not callable). Use `.is` for terminal checks:
*
* ```typescript
* // Terminal check (explicit .is)
* type T = Ts.Assert.exact.awaited.is<number, Promise<number>>
* Ts.Assert.exact.returned.is<string>()(fn)
*
* // Chaining (nest extractors)
* type T = Ts.Assert.exact.returned.awaited<User, () => Promise<User>>
* Ts.Assert.sub.awaited.array<number>()(Promise.resolve([1, 2, 3]))
*
```
*
* ### Functions
* - `.parameter<X, F>` - First parameter (most common)
* - `.parameter1-5<X, F>` - Specific parameter position
* - `.parameters<[...], F>` - Full parameter tuple
*
* ```typescript
* type T = Ts.Assert.exact.parameter<string, (x: string) => void>
* type T = Ts.Assert.sub.parameter2<number, (a: string, b: number) => void>
*
```
*
* ### Objects
* - `.properties<Props, T>` - Check specific properties (ignores others)
*
* ```typescript
* type Config = { id: string; name: string; debug: boolean }
* type T = Ts.Assert.exact.properties<{ id: string }, Config> // ✓ Pass
*
```
*
* ### Modifiers
* - `.noExcess<A, B>` - Additionally check for no excess properties
*
* **`sub.noExcess`** - Most common use case (config validation with narrowing):
* ```typescript
* type Options = { timeout?: number; retry?: boolean }
* type T = Ts.Assert.sub.noExcess<Options, { timeout: 5000, retry: true }> // ✓ Allows literals
* type T = Ts.Assert.sub.noExcess<Options, { timeout: 5000, retrys: true }> // ✗ Catches typo!
*
```
*
* **`equiv.noExcess`** - Special case (optional property typos in equiv types):
* ```typescript
* type Schema = { id: number; email?: string }
* type Response = { id: number; emial?: string } // Typo!
* type T = Ts.Assert.equiv<Schema, Response> // ✓ Pass (mutually assignable)
* type T = Ts.Assert.equiv.noExcess<Schema, Response> // ✗ Fail (catches typo!)
*
```
*
* ## Negation
*
* The `.not` namespace mirrors the entire API structure:
*
* ```typescript
* // Negate any assertion
* type T = Ts.Assert.not.exact<string, number> // ✓ Pass (different)
* type T = Ts.Assert.not.sub.awaited<X, Promise<Y>> // ✓ Pass if Y doesn't extend X
* Ts.Assert.not.exact.returned.awaited<X>()(fn) // Value level
*
```
*
* ## Value Level vs Type Level
*
* **Type Level**: Use relations and extractors directly as types
* ```typescript
* type T = Ts.Assert.exact<A, B>
* type T = Ts.Assert.sub.awaited<X, Promise<Y>>
*
```
*
* **Value Level**: Relations require `.is`, extractors work directly
* ```typescript
* // Relations need .is for identity
* Ts.Assert.exact.of.as<string>()(value) // ✓ Use .is
* Ts.Assert.exact<string>()(value) // ✗ Error - exact is not callable!
*
* // Extractors work directly
* Ts.Assert.exact.awaited<X>()(promise) // ✓ Works
*
* // Chained extractors use .is for terminal
* Ts.Assert.exact.returned.is<X>()(fn) // Terminal check
* Ts.Assert.exact.returned.awaited<X>()(fn) // Chained check
*
```
*
* ## Why `.is` for Identity?
*
* Relations (`exact`, `equiv`, `sub`) are **namespace-only** at value level to avoid
* callable interfaces which pollute autocomplete with function properties (`call`, `apply`,
* `bind`, `length`, `name`, etc.). Using `.is` keeps autocomplete clean and consistent.
*
* ## Type-Level Diff
*
* When comparing object types, failed assertions automatically include a `diff` field:
*
* ```typescript
* type Expected = { id: string; name: string; age: number }
* type Actual = { id: number; name: string; email: string }
*
* type T = Ts.Assert.exact<Expected, Actual>
* // Error includes:
* // diff: {
* // missing: { age: number }
* // excess: { email: string }
* // mismatched: { id: { expected: string, actual: number } }
* // }
*
```
*
* ## Configuration
*
* Assertion behavior can be configured via global settings.
* See {@link KitLibrarySettings.Ts.Assert} for available options.
*
* @example
* ```typescript
* // Enable strict linting in your project
* // types/kit-settings.d.ts
* declare global {
* namespace KitLibrarySettings {
* namespace Ts {
* namespace Test {
* interface Settings {
* lintBidForExactPossibility: true
* }
* }
* }
* }
* }
* export { }
*
```
*/Import
import { Ts } from '@wollybeard/kit'
// Access via namespace
Ts.Assertimport { Assert } from '@wollybeard/kit/ts'Namespaces
| Namespace | Description |
|---|---|
array | — |
awaited | — |
equiv | — |
exact | — |
not | — |
parameter1 | — |
parameter2 | — |
parameter3 | — |
parameter4 | — |
parameter5 | — |
parameters | — |
returned | — |
sub | — |
Utils
[T] StaticErrorAssertion
type StaticErrorAssertion<
$Message extends string = string,
$Expected = unknown,
$Actual = unknown,
$MetaInput extends MetaInput = never,
___$ErrorKeyLength extends number =
KitLibrarySettings.Ts.Error['errorKeyLength'],
> = Ts.Err.StaticError<
$Message,
{
expected: $Expected
actual: $Actual
} & NormalizeMetaInput<$MetaInput>,
readonly ['root', 'assert', ...string[]]
>Represents a static assertion error at the type level, optimized for type testing.
This is a simpler, more focused error type compared to Ts.StaticError. It's specifically designed for type assertions where you need to communicate expected vs. actual types.
Supports three forms of metadata:
- Single string tip:
StaticErrorAssertion<'msg', E, A, 'tip'> - Tuple of tips:
StaticErrorAssertion<'msg', E, A, ['tip1', 'tip2']> - Metadata object:
StaticErrorAssertion<'msg', E, A, { custom: 'data' }> - Object with tip:
StaticErrorAssertion<'msg', E, A, { tip: 'advice', ...meta }>
$Message
- A string literal type describing the assertion failure
$Expected
- The expected type
$Actual
- The actual type that was provided
$MetaInput
- Optional metadata: string tip, tuple of tips, or object with custom fields
Examples:
// Simple error with message only
type E1 = StaticErrorAssertion<'Types mismatch', string, number>
// With a single tip
type E2 = StaticErrorAssertion<
'Types mismatch',
string,
number,
'Use String() to convert'
>
// With multiple tips
type E3 = StaticErrorAssertion<
'Types mismatch',
string,
number,
['Tip 1', 'Tip 2']
>
// With metadata object
type E4 = StaticErrorAssertion<
'Types mismatch',
string,
number,
{ operation: 'concat' }
>
// With tip and metadata
type E5 = StaticErrorAssertion<
'Types mismatch',
string,
number,
{ tip: 'Use String()'; diff_missing: { x: number } }
>Other
[C] any
InputActualForUnaryRelatorNarrow<State.Empty, 'any'>[C] unknown
InputActualForUnaryRelatorNarrow<State.Empty, 'unknown'>[C] never
InputActualForUnaryRelatorNarrow<State.Empty, 'never'>[C] empty
InputActualForUnaryRelatorNarrow<State.Empty, 'empty'>[C] on
InputActualAsValueNarrow<State.Empty>[C] onAs
InputActualAsType<State.Empty>[C] inferNarrow
Builder<State.SetInferMode<State.Empty, 'narrow'>>[C] inferWide
Builder<State.SetInferMode<State.Empty, 'wide'>>[C] inferAuto
Builder<State.SetInferMode<State.Empty, 'auto'>>[C] setInfer
;(<$Mode>(mode: $Mode) => Builder<State.SetInferMode<State.Empty, $Mode>>)[T] Case
type Case<$Result extends never> = $ResultType-level test assertion that requires the result to be never (no error). Used in type-level test suites to ensure a type evaluates to never (success).
Generally prefer value-level API instead.
Problem: Individual Case<> assertions don't actually catch type errors at compile time due to internal casting. Errors only appear when wrapped in Cases<>, which has its own issues.
Better Alternative: Use value-level API which reports ALL errors simultaneously:
// ❌ Type-level
- doesn't catch errors reliably
type _ = Ts.Assert.Case<Assert.exact<string, number>>
// May silently pass!
// ✅ Value-level
- shows all errors
Assert.exact.ofAs<string>().onAs<number>()
// Error shown immediatelyExamples:
type MyTests = [
Ts.Assert.Case<Equal<string, string>>, // OK - evaluates to never (success)
Ts.Assert.Case<Equal<string, number>>, // Error - doesn't extend never (returns error)
][T] Cases
type Cases<
_T1 extends never = never,
_T2 extends never = never,
_T3 extends never = never,
_T4 extends never = never,
_T5 extends never = never,
_T6 extends never = never,
_T7 extends never = never,
_T8 extends never = never,
_T9 extends never = never,
_T10 extends never = never,
_T11 extends never = never,
_T12 extends never = never,
_T13 extends never = never,
_T14 extends never = never,
_T15 extends never = never,
_T16 extends never = never,
_T17 extends never = never,
_T18 extends never = never,
_T19 extends never = never,
_T20 extends never = never,
_T21 extends never = never,
_T22 extends never = never,
_T23 extends never = never,
_T24 extends never = never,
_T25 extends never = never,
_T26 extends never = never,
_T27 extends never = never,
_T28 extends never = never,
_T29 extends never = never,
_T30 extends never = never,
_T31 extends never = never,
_T32 extends never = never,
_T33 extends never = never,
_T34 extends never = never,
_T35 extends never = never,
_T36 extends never = never,
_T37 extends never = never,
_T38 extends never = never,
_T39 extends never = never,
_T40 extends never = never,
_T41 extends never = never,
_T42 extends never = never,
_T43 extends never = never,
_T44 extends never = never,
_T45 extends never = never,
_T46 extends never = never,
_T47 extends never = never,
_T48 extends never = never,
_T49 extends never = never,
_T50 extends never = never,
_T51 extends never = never,
_T52 extends never = never,
_T53 extends never = never,
_T54 extends never = never,
_T55 extends never = never,
_T56 extends never = never,
_T57 extends never = never,
_T58 extends never = never,
_T59 extends never = never,
_T60 extends never = never,
_T61 extends never = never,
_T62 extends never = never,
_T63 extends never = never,
_T64 extends never = never,
_T65 extends never = never,
_T66 extends never = never,
_T67 extends never = never,
_T68 extends never = never,
_T69 extends never = never,
_T70 extends never = never,
_T71 extends never = never,
_T72 extends never = never,
_T73 extends never = never,
_T74 extends never = never,
_T75 extends never = never,
_T76 extends never = never,
_T77 extends never = never,
_T78 extends never = never,
_T79 extends never = never,
_T80 extends never = never,
_T81 extends never = never,
_T82 extends never = never,
_T83 extends never = never,
_T84 extends never = never,
_T85 extends never = never,
_T86 extends never = never,
_T87 extends never = never,
_T88 extends never = never,
_T89 extends never = never,
_T90 extends never = never,
_T91 extends never = never,
_T92 extends never = never,
_T93 extends never = never,
_T94 extends never = never,
_T95 extends never = never,
_T96 extends never = never,
_T97 extends never = never,
_T98 extends never = never,
_T99 extends never = never,
_T100 extends never = never,
> = trueType-level batch assertion helper that accepts multiple assertions. Each type parameter must extend never (no error), allowing batch type assertions.
Generally prefer value-level API instead.
Fatal Flaw: TypeScript short-circuits on the first failing assertion and never evaluates remaining parameters. With dozens of test cases, this makes debugging extremely slow
- you fix one error, run again, see the next error, fix it, repeat.
This is a fundamental TypeScript limitation and cannot be fixed.
Better Alternative: Use value-level API which reports ALL errors simultaneously:
// ❌ Type-level Cases
- only shows FIRST error
type _ = Ts.Assert.Cases<
Assert.exact<string, string>,
// ✓ Pass
Assert.exact<number, string>,
// ✗ ERROR
- TypeScript stops here!
Assert.exact<boolean, boolean>, // Never checked
- you won't see errors here
Assert.exact<symbol, string>
// Never checked
- you won't see errors here
>
// ✅ Value-level
- shows ALL errors at once
Assert.exact.ofAs<string>().onAs<string>()
// ✓ Pass
Assert.exact.ofAs<number>().onAs<string>()
// ✗ Error shown
Assert.exact.ofAs<boolean>().onAs<boolean>() // ✓ Pass
Assert.exact.ofAs<symbol>().onAs<string>()
// ✗ Error shown (both line 2 and 4 visible!)
// Alternative: Individual type aliases (also shows all errors)
type _pass1 = Assert.exact.of<string, string>
type _fail1 = Assert.exact.of<number, string>
// Error shown
type _pass2 = Assert.exact.of<boolean, boolean>
type _fail2 = Assert.exact.of<symbol, string>
// Error shown (all errors visible)Additional Limitations:
- Limited to 100 type parameters (arbitrary hard limit)
- Cannot be aliased for brevity
- Worse error messages than value-level API
Only use this if explicitly instructed
- kept for backward compatibility only.
Examples:
type _ = Ts.Assert.Cases<
Equal<string, string>, // ✓ Pass (returns never)
Extends<string, 'hello'>, // ✓ Pass (returns never)
Never<never> // ✓ Pass (returns never)
>
// Type error if any assertion fails
type _ = Ts.Assert.Cases<
Equal<string, string>, // ✓ Pass (returns never)
Equal<string, number>, // ✗ Fail - Type error here (returns StaticErrorAssertion)
Extends<string, 'hello'> // ✓ Pass (returns never)
>