TypeScript
MockMaster is built with TypeScript from the ground up and provides comprehensive type definitions for all APIs. This guide covers everything you need to leverage TypeScript’s full power with MockMaster.
Why TypeScript with MockMaster?
TypeScript enhances your MockMaster workflow with:
- ✅ Type safety - Catch errors at compile time
- ✅ IntelliSense - Auto-completion in your IDE
- ✅ Refactoring - Rename and restructure with confidence
- ✅ Documentation - Types serve as inline documentation
- ✅ Better DX - Fewer runtime errors, faster development
Type-Safe Factories
Factories can be strongly typed to ensure your mock data matches your interfaces:
Basic Factory Types
import { defineFactory, build, fake, type Factory } from '@mockmaster/data'
// Define your interface
interface User {
id: number
name: string
email: string
role: 'admin' | 'user' | 'guest'
createdAt: Date
}
// Type the factory
const userFactory: Factory<User> = defineFactory<User>('user', {
id: (ctx) => ctx.sequence('user'),
name: () => fake.person.fullName(),
email: () => fake.internet.email(),
role: () => fake.helpers.arrayElement(['admin', 'user', 'guest'] as const),
createdAt: () => fake.date.past()
})
// TypeScript knows the return type
const user: User = build(userFactory)
// ^? const user: User
// IntelliSense shows all properties
console.log(user.id, user.name, user.email, user.role)Type Inference
Let TypeScript infer types when possible:
// Without explicit type annotation - TypeScript infers the type
const productFactory = defineFactory('product', {
id: (ctx) => ctx.sequence('product'),
name: () => fake.commerce.productName(),
price: () => parseFloat(fake.commerce.price()),
inStock: () => fake.datatype.boolean()
})
// Type is inferred from factory definition
const product = build(productFactory)
// ^? const product: { id: number; name: string; price: number; inStock: boolean }Complex Nested Types
interface Address {
street: string
city: string
state: string
zipCode: string
country: string
}
interface Company {
id: number
name: string
industry: string
employees: number
headquarters: Address
}
const addressFactory = defineFactory<Address>('address', {
street: () => fake.location.streetAddress(),
city: () => fake.location.city(),
state: () => fake.location.state(),
zipCode: () => fake.location.zipCode(),
country: () => fake.location.country()
})
const companyFactory = defineFactory<Company>('company', {
id: (ctx) => ctx.sequence('company'),
name: () => fake.company.name(),
industry: () => fake.commerce.department(),
employees: () => fake.number.int({ min: 10, max: 10000 }),
headquarters: () => build(addressFactory) // Nested factory
})
// Fully type-safe
const company: Company = build(companyFactory)
console.log(company.headquarters.city) // TypeScript knows all nested propertiesType-Safe Scenarios
Typed Recordings
Create recordings with explicit response types:
import { createRecording, type Recording } from '@mockmaster/msw-adapter'
interface UserResponse {
id: number
name: string
email: string
verified: boolean
}
// Type the response body
const recording = createRecording<UserResponse>(
{
method: 'GET',
url: 'https://api.example.com/users/123',
path: '/users/:id',
timestamp: Date.now()
},
{
status: 200,
body: {
id: 123,
name: 'John Doe',
email: 'john@example.com',
verified: true
},
timestamp: Date.now()
}
)
// TypeScript validates the body matches UserResponse
// This would error: body: { id: '123' } // id should be number, not stringTyped Handlers
Replay handlers can return typed responses:
import { createReplayHandler, type ReplayHandler } from '@mockmaster/msw-adapter'
interface ApiResponse<T> {
status: number
body: T
headers?: Record<string, string>
}
const handler = createReplayHandler(scenario)
// Type the response
const response = handler({ method: 'GET', path: '/users/123' }) as ApiResponse<UserResponse> | undefined
if (response) {
// TypeScript knows the shape of response.body
const userId: number = response.body.id
const userName: string = response.body.name
}Generic Scenarios
Create reusable generic functions for scenarios:
import { Scenario, Recording, createScenario, addRecordingToScenario } from '@mockmaster/msw-adapter'
// Generic function to create a scenario with typed recordings
function createTypedScenario<T>(
name: string,
description: string,
recordings: Array<Recording<T>>
): Scenario {
let scenario = createScenario(name, description)
for (const recording of recordings) {
scenario = addRecordingToScenario(scenario, recording)
}
return scenario
}
// Use it
interface Product {
id: number
name: string
price: number
}
const productRecordings: Array<Recording<Product[]>> = [
createRecording<Product[]>(
{ method: 'GET', path: '/products', timestamp: Date.now() },
{
status: 200,
body: [
{ id: 1, name: 'Widget', price: 9.99 },
{ id: 2, name: 'Gadget', price: 19.99 }
],
timestamp: Date.now()
}
)
]
const productScenario = createTypedScenario('products', 'Product API', productRecordings)Type Guards and Assertions
Use type guards to safely handle responses:
// Type guard for successful responses
function isSuccessResponse<T>(
response: { status: number; body: T } | undefined
): response is { status: number; body: T } {
return response !== undefined && response.status >= 200 && response.status < 300
}
// Usage
const response = handler({ method: 'GET', path: '/users/123' })
if (isSuccessResponse(response)) {
// TypeScript knows response is defined here
console.log(response.body)
} else {
console.error('Request failed or no match')
}Type Assertions
// When you know the response type
const response = handler({ method: 'GET', path: '/users' })!
const users = response.body as User[]
// Safer with type guard
function assertUserArray(body: unknown): asserts body is User[] {
if (!Array.isArray(body)) {
throw new Error('Expected array')
}
}
const response2 = handler({ method: 'GET', path: '/users' })
if (response2) {
assertUserArray(response2.body)
// TypeScript now knows response2.body is User[]
response2.body.forEach(user => console.log(user.name))
}Discriminated Unions
Handle different response types with discriminated unions:
// Define response types
type ApiSuccess<T> = {
status: 'success'
data: T
}
type ApiError = {
status: 'error'
message: string
code: string
}
type ApiResponse<T> = ApiSuccess<T> | ApiError
// Type guard
function isSuccessResponse<T>(response: ApiResponse<T>): response is ApiSuccess<T> {
return response.status === 'success'
}
// Factory
interface UserData {
id: number
name: string
}
const successResponse = createRecording<ApiResponse<UserData>>(
{ method: 'GET', path: '/users/1', timestamp: Date.now() },
{
status: 200,
body: {
status: 'success',
data: { id: 1, name: 'Alice' }
},
timestamp: Date.now()
}
)
const errorResponse = createRecording<ApiResponse<UserData>>(
{ method: 'GET', path: '/users/999', timestamp: Date.now() },
{
status: 404,
body: {
status: 'error',
message: 'User not found',
code: 'USER_NOT_FOUND'
},
timestamp: Date.now()
}
)
// Usage with type narrowing
const handler = createReplayHandler(scenario)
const response = handler({ method: 'GET', path: '/users/1' })
if (response && isSuccessResponse(response.body)) {
console.log(response.body.data.name) // TypeScript knows this is UserData
} else if (response) {
console.error(response.body.message) // TypeScript knows this is ApiError
}Utility Types
Create utility types for common patterns:
// Extract response type from a recording
type ResponseBody<T> = T extends Recording<infer R> ? R : never
// Example
const userRecording = createRecording<User>({...}, {...})
type UserResponseType = ResponseBody<typeof userRecording> // User
// Generic scenario builder
type ScenarioBuilder<T> = {
name: string
recordings: Array<Recording<T>>
build: () => Scenario
}
function createScenarioBuilder<T>(name: string, description: string): ScenarioBuilder<T> {
const recordings: Array<Recording<T>> = []
return {
name,
recordings,
build() {
let scenario = createScenario(name, description)
for (const recording of recordings) {
scenario = addRecordingToScenario(scenario, recording)
}
return scenario
}
}
}
// Use it
const builder = createScenarioBuilder<User>('users', 'User API')
builder.recordings.push(
createRecording<User>({...}, {...})
)
const scenario = builder.build()Strict Mode
Enable strict TypeScript checks for maximum safety:
tsconfig.json
{
"compilerOptions": {
"strict": true, // Enable all strict checks
"strictNullChecks": true, // Enforce null/undefined handling
"strictFunctionTypes": true, // Strict function type checking
"noImplicitAny": true, // No implicit 'any' types
"noImplicitThis": true, // 'this' must be explicitly typed
"noUnusedLocals": true, // Error on unused variables
"noUnusedParameters": true, // Error on unused parameters
"noImplicitReturns": true, // All code paths must return
"noFallthroughCasesInSwitch": true
}
}Handling Undefined Responses
With strict null checks, handle undefined responses explicitly:
// Bad - will error in strict mode
const response = handler({ method: 'GET', path: '/users' })
console.log(response.body) // Error: Object is possibly 'undefined'
// Good - explicit check
const response = handler({ method: 'GET', path: '/users' })
if (response) {
console.log(response.body) // Safe
}
// Good - optional chaining
console.log(response?.body)
// Good - nullish coalescing
const users = response?.body ?? []Type-Safe Testing Patterns
Vitest with TypeScript
import { describe, it, expect, beforeEach } from 'vitest'
import { readScenario } from '@mockmaster/cli'
import { createReplayHandler, type ReplayHandler } from '@mockmaster/msw-adapter'
interface User {
id: number
name: string
email: string
}
describe('User API', () => {
let handler: ReplayHandler
beforeEach(async () => {
const scenario = await readScenario('./scenarios', 'user-api')
handler = createReplayHandler(scenario)
})
it('returns typed user data', () => {
const response = handler({ method: 'GET', path: '/users/1' })
expect(response).toBeDefined()
// Type assertion for test
const user = response!.body as User
// Now TypeScript knows the shape
expect(user.id).toBe(1)
expect(user.name).toMatch(/\w+/)
expect(user.email).toContain('@')
})
it('handles array responses', () => {
const response = handler({ method: 'GET', path: '/users' })
expect(response).toBeDefined()
const users = response!.body as User[]
expect(Array.isArray(users)).toBe(true)
expect(users[0]).toHaveProperty('id')
expect(users[0]).toHaveProperty('name')
})
})Jest with TypeScript
import { readScenario } from '@mockmaster/cli'
import { createReplayHandler } from '@mockmaster/msw-adapter'
interface Product {
id: number
name: string
price: number
inStock: boolean
}
describe('Product API', () => {
let handler: ReturnType<typeof createReplayHandler>
beforeEach(async () => {
const scenario = await readScenario('./scenarios', 'products')
handler = createReplayHandler(scenario)
})
test('fetches products with correct types', () => {
const response = handler({ method: 'GET', path: '/products' })
expect(response).toBeDefined()
const products = response!.body as Product[]
products.forEach(product => {
expect(typeof product.id).toBe('number')
expect(typeof product.name).toBe('string')
expect(typeof product.price).toBe('number')
expect(typeof product.inStock).toBe('boolean')
})
})
})Advanced Patterns
Builder Pattern with Types
class TypedScenarioBuilder<T> {
private scenario: Scenario
constructor(name: string, description: string) {
this.scenario = createScenario(name, description)
}
addRecording(
method: string,
path: string,
status: number,
body: T
): this {
const recording = createRecording<T>(
{ method, url: `https://api.example.com${path}`, path, timestamp: Date.now() },
{ status, body, timestamp: Date.now() }
)
this.scenario = addRecordingToScenario(this.scenario, recording)
return this
}
build(): Scenario {
return this.scenario
}
}
// Usage
interface User {
id: number
name: string
}
const scenario = new TypedScenarioBuilder<User>('users', 'User API')
.addRecording('GET', '/users/1', 200, { id: 1, name: 'Alice' })
.addRecording('GET', '/users/2', 200, { id: 2, name: 'Bob' })
.build()Generic Factory Functions
import { Factory, FactoryDefinition } from '@mockmaster/data'
// Generic factory creator
function createApiFactory<T extends Record<string, any>>(
name: string,
definition: FactoryDefinition<T>
): Factory<T> {
return defineFactory<T>(name, definition)
}
// Use with specific types
interface Post {
id: number
title: string
body: string
authorId: number
}
const postFactory = createApiFactory<Post>('post', {
id: (ctx) => ctx.sequence('post'),
title: () => fake.lorem.sentence(),
body: () => fake.lorem.paragraphs(3),
authorId: () => fake.number.int({ min: 1, max: 100 })
})
const post: Post = build(postFactory)Type Exports
MockMaster exports all necessary types:
// Core types
import type {
PathMatcher,
PathParams,
MatchResult
} from '@mockmaster/core'
// Data types
import type {
Factory,
FactoryDefinition,
FactoryContext,
BuildOptions
} from '@mockmaster/data'
// MSW Adapter types
import type {
Scenario,
Recording,
RequestDetails,
ResponseDetails,
ReplayHandler
} from '@mockmaster/msw-adapter'
// OpenAPI types
import type {
OpenAPISpec,
SchemaObject,
PathItemObject,
OperationObject,
ResponseObject
} from '@mockmaster/openapi'Best Practices
1. Define Interfaces First
// Define your data shapes
interface User {
id: number
name: string
email: string
}
// Then create factories/recordings
const userFactory = defineFactory<User>(...)
const userRecording = createRecording<User>(...)2. Use Type Guards
function isUser(obj: unknown): obj is User {
return (
typeof obj === 'object' &&
obj !== null &&
'id' in obj &&
'name' in obj &&
'email' in obj
)
}3. Avoid any
// Bad
const data: any = response.body
// Good
const data = response.body as User
// or
const data: User = response.body4. Use Generics
// Reusable typed functions
function createTestHandler<T>(scenarioName: string): ReplayHandler {
const scenario = await readScenario('./scenarios', scenarioName)
return createReplayHandler(scenario)
}Common TypeScript Errors
Error: Type ‘undefined’ is not assignable
// Problem
const response = handler({ method: 'GET', path: '/users' })
const users = response.body // Error: Object is possibly 'undefined'
// Solution 1: Type guard
if (response) {
const users = response.body // OK
}
// Solution 2: Non-null assertion (use carefully)
const users = response!.body
// Solution 3: Optional chaining
const users = response?.bodyError: Property does not exist on type
// Problem
const user = response.body
console.log(user.name) // Error: Property 'name' does not exist
// Solution: Type assertion
const user = response.body as User
console.log(user.name) // OKKey Takeaways
- ✅ Type your factories with interfaces for full type safety
- ✅ Use generics for reusable, type-safe code
- ✅ Leverage type guards for safe runtime checks
- ✅ Enable strict mode in tsconfig.json
- ✅ Export and reuse type definitions
- ✅ Use TypeScript’s inference when possible
- ✅ Handle undefined responses explicitly
MockMaster + TypeScript = Type-safe, confident testing!