Skip to content

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:

typescript
// 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:

typescript
// Type-only check - no value
Ts.Assert.exact<string, string>()

// Fails - requires error parameter
Ts.Assert.exact<string, number>() // ❌ Expected 1 arguments, but got 0

Signature: <Expected, Actual>() => void (passes) or <Expected, Actual>(error) => void (fails)

Error Feedback

When assertions fail, errors appear in parameter types:

typescript
// 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:

typescript
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:

typescript
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):

typescript
exact<A, B> // A extends B AND B extends A

sub

Actual must be subtype of expected:

typescript
sub<A, B> // B extends A

equiv

Types are equivalent (ignoring excess properties):

typescript
equiv<A, B> // A and B have same structure, allow excess

not.*

Negated relations:

typescript
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:

typescript
awaited.is<number>()(Promise.resolve(1)) // OK
awaited.is<string>()(Promise.resolve(1)) // ❌

returned

Extract and assert on return type:

typescript
returned.is<number>()(() => 1) // OK
returned.awaited<string>()(() => Promise.resolve('x')) // OK

parameter

Assert on function parameter type:

typescript
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:

typescript
array<string>()(['a', 'b']) // OK

tuple

Assert exact tuple type:

typescript
tuple<[string, number]>()(['a', 1]) // OK

indexed

Extract and assert on index signature value type:

typescript
// 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 signature

Properties

properties

Assert object has required properties (allows excess):

typescript
properties<{ id: string }>()({ id: '1', name: 'test' }) // OK

Type-Level Testing

For batch type assertions in test files:

typescript
// 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:

typescript
const x = 1 as const
type T = typeof x // 1 (not number)

Ts.Assert.exact<1>()(x) // OK - x is type 1

Use .const() method for explicit literal preservation:

typescript
Ts.Assert.exact<1>().const(1) // Preserves literal 1

Never Detection

Passing never to non-never assertions requires explicit error parameter:

typescript
Ts.Assert.exact<number>()(null as never)
// ❌ Requires: (error: NeverNotAllowedError)

Ts.Assert.exact<never>()(null as never) // OK

Error Messages

Error objects use aligned keys for readability:

typescript
{
  ERROR_________: 'Types are not exactly equal'
  expected______: string
  actual________: number
}

Key length configured via KitLibrarySettings.Ts.Assert.errorKeyLength.

ts
/**
 * 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

typescript
import { Ts } from '@wollybeard/kit'

// Access via namespace
Ts.Assert
typescript
import { Assert } from '@wollybeard/kit/ts'

Namespaces

NamespaceDescription
array
awaited
equiv
exact
not
parameter1
parameter2
parameter3
parameter4
parameter5
parameters
returned
sub

Utils

[T] StaticErrorAssertion

typescript
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:

typescript
// 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

typescript
InputActualForUnaryRelatorNarrow<State.Empty, 'any'>

[C] unknown

typescript
InputActualForUnaryRelatorNarrow<State.Empty, 'unknown'>

[C] never

typescript
InputActualForUnaryRelatorNarrow<State.Empty, 'never'>

[C] empty

typescript
InputActualForUnaryRelatorNarrow<State.Empty, 'empty'>

[C] on

typescript
InputActualAsValueNarrow<State.Empty>

[C] onAs

typescript
InputActualAsType<State.Empty>

[C] inferNarrow

typescript
Builder<State.SetInferMode<State.Empty, 'narrow'>>

[C] inferWide

typescript
Builder<State.SetInferMode<State.Empty, 'wide'>>

[C] inferAuto

typescript
Builder<State.SetInferMode<State.Empty, 'auto'>>

[C] setInfer

typescript
;(<$Mode>(mode: $Mode) => Builder<State.SetInferMode<State.Empty, $Mode>>)

[T] Case

typescript
type Case<$Result extends never> = $Result

Type-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:

ts
// ❌ 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 immediately

Examples:

typescript
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

typescript
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,
> = true

Type-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:

ts
// ❌ 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:

typescript
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)
>