Skip to content

TypeScript

deride is TypeScript-first. Every public API is generic over the shape you're mocking, so setup methods are constrained to the real method signatures and return types.

Generic inference from an interface

typescript
interface Service {
  fetch(url: string): Promise<string>
  process(data: string): void
}

const mock = stub<Service>(['fetch', 'process'])

mock.setup.fetch.toResolveWith('response')   // ✓ — string is the resolved type
mock.setup.fetch.toResolveWith(123)          // ✗ — type error: number not assignable to string
mock.setup.process.toReturn(undefined)       // ✓ — void

The type Wrapped<Service> is inferred automatically. You can annotate explicitly if you prefer:

typescript
const mock: Wrapped<Service> = stub<Service>(['fetch', 'process'])

toResolveWith unwraps Promise<T>

For async methods, toResolveWith(v) expects the resolved type — not the Promise itself:

typescript
interface Api {
  get(): Promise<{ id: number }>
}

const mock = stub<Api>(['get'])
mock.setup.get.toResolveWith({ id: 1 })            // ✓
mock.setup.get.toResolveWith(Promise.resolve(...))  // ✗ — expects { id: number }, not Promise

toDoThis(fn) constrains the callback signature

typescript
interface Calc {
  sum(a: number, b: number): number
}

const mock = stub<Calc>(['sum'])
mock.setup.sum.toDoThis((a, b) => a + b)           // ✓ — (a: number, b: number) => number
mock.setup.sum.toDoThis((a, b) => String(a + b))    // ✗ — returns string, expected number

Argument matchers preserve type safety

Matchers in when() / withArg() / withArgs() interleave with typed positional args:

typescript
mock.setup.sum.when(match.number, match.number).toReturn(0)
mock.expect.sum.called.withArgs(1, match.number)

match.* matcher types are Matcher<T> — you can type-annotate match.where with a specific parameter type:

typescript
mock.setup.log.when(match.where<string>(s => s.startsWith('['))).toReturn(undefined)

Escape hatch — as any

For deliberately-wrong values in error-path tests:

typescript
// You want to test that consumers handle a null response gracefully,
// even though the type says fetch returns string
mock.setup.fetch.toResolveWith(null as any)

as any is the officially-supported escape hatch. It only affects the one call it annotates.

Public type exports

Everything you might want is exported from the main entry:

typescript
import type {
  // Shape of a mocked object
  Wrapped,

  // Setup surface
  TypedMockSetup, MockSetup,

  // Expect surface
  MockExpect, CalledExpect, InvocationExpect,

  // Spy surface
  MethodSpy, CallRecord,

  // Lifecycle
  MockSnapshot, Sandbox,

  // Factories
  MockedFunction, MockedClass,

  // Matchers
  Matcher,

  // Options
  Options,
} from 'deride'

Per-method types via index-type lookup:

typescript
import type { Wrapped } from 'deride'
import type { Service } from '../src/service'

type ServiceSetup = Wrapped<Service>['setup']
type ServiceGetSetup = ServiceSetup['get']

Types for stub.class

typescript
import { stub, type MockedClass } from 'deride'

class Database {
  constructor(public conn: string) {}
  async query(sql: string): Promise<unknown[]> { return [] }
}

const MockedDb: MockedClass<typeof Database> = stub.class(Database)

const inst = new MockedDb('conn-string')   // inst: Wrapped<Database>
MockedDb.expect.constructor.called.once()
MockedDb.instances                          // readonly Wrapped<Database>[]

Using deride with strict TypeScript

All of deride's types are designed to work under strict: true — including noImplicitAny, strictNullChecks, and exactOptionalPropertyTypes. If you hit a type error that surprises you, check these first:

  • exactOptionalPropertyTypeswithReturn(undefined) is valid; but if your method's return type is string, TypeScript won't let you pass undefined. Use match.any or cast as any.
  • Generic width — when passing stub<Service> with a heavy union return type, inference can be lazy. Annotating the left-hand side (const mock: Wrapped<Service> = …) usually helps.
  • noUnusedParameters in toDoThis — use _ prefixed args: toDoThis((_a, b) => b + 1).

Type-level testing

deride ships with type-level tests under test/types.test.ts exercising the constraints. If you're extending the library or building custom helpers, adding a few type-level assertions is a good idea — e.g. using tsd or a hand-rolled // @ts-expect-error block:

typescript
// @ts-expect-error — should reject wrong return type
mock.setup.fetch.toResolveWith(123)

IDE experience

Every public API has JSDoc comments, surfaced by editors as hover-docs:

  • setup.method.toReturn(value: R) — "Return a fixed value when invoked. Cast as any to return an invalid type."
  • expect.method.called.withReturn(expected) — "Assert at least one call returned a value matching expected (value or matcher)."

The jsdoc/require-jsdoc lint rule enforces this on every public export, so it stays current as the API evolves.

Released under the MIT License.