Integration Guide
Integrate FoundryKit types with popular frameworks and libraries
Integration Guide
Learn how to integrate FoundryKit types with popular frameworks, libraries, and tools to enhance type safety across your entire development stack.
React Integration
Component Props with Types
Seamlessly integrate FoundryKit types with React components.
import React, { forwardRef } from 'react'
import type {
ComponentProps,
ComponentRef,
ForwardRefComponent,
WithChildren,
WithClassName,
EventHandler,
Optional
} from '@foundrykit/types'
// Base component with FoundryKit types
interface BaseButtonProps extends WithClassName {
variant?: 'primary' | 'secondary' | 'danger'
size?: 'sm' | 'md' | 'lg'
disabled?: boolean
loading?: boolean
onClick?: EventHandler<React.MouseEvent>
}
// Forward ref component with full type safety
const Button: ForwardRefComponent<'button', BaseButtonProps> = forwardRef(
({ variant = 'primary', size = 'md', disabled, loading, className, children, ...props }, ref) => {
return (
<button
ref={ref}
className={cn(
'btn',
`btn-${variant}`,
`btn-${size}`,
{ 'btn-disabled': disabled || loading },
className
)}
disabled={disabled || loading}
{...props}
>
{loading ? 'Loading...' : children}
</button>
)
}
)
// Optional props pattern
interface UserCardProps extends WithChildren, WithClassName {
user: Optional<User, 'avatar' | 'bio'>
onEdit?: EventHandler<React.MouseEvent>
showActions?: boolean
}
function UserCard({ user, onEdit, showActions = true, className, children }: UserCardProps) {
return (
<div className={cn('user-card', className)}>
<div className="user-info">
{user.avatar && <img src={user.avatar} alt="Avatar" />}
<div>
<h3>{user.name}</h3>
<p>{user.email}</p>
{user.bio && <p className="bio">{user.bio}</p>}
</div>
</div>
{showActions && (
<div className="actions">
<button onClick={onEdit}>Edit</button>
</div>
)}
{children}
</div>
)
}
Custom Hooks with Types
Create type-safe custom hooks.
import { useState, useCallback, useEffect } from 'react'
import type {
ApiResponse,
ApiError,
PaginatedResponse,
FormData,
ValidationResult
} from '@foundrykit/types'
// Generic data fetching hook
interface UseApiOptions<T> {
enabled?: boolean
onSuccess?: (data: T) => void
onError?: (error: ApiError) => void
}
interface UseApiResult<T> {
data: T | null
loading: boolean
error: ApiError | null
refetch: () => Promise<void>
}
function useApi<T>(
fetcher: () => Promise<ApiResponse<T>>,
options: UseApiOptions<T> = {}
): UseApiResult<T> {
const [data, setData] = useState<T | null>(null)
const [loading, setLoading] = useState(false)
const [error, setError] = useState<ApiError | null>(null)
const fetchData = useCallback(async () => {
if (options.enabled === false) return
setLoading(true)
setError(null)
try {
const response = await fetcher()
setData(response.data)
options.onSuccess?.(response.data)
} catch (err) {
const apiError = err as ApiError
setError(apiError)
options.onError?.(apiError)
} finally {
setLoading(false)
}
}, [fetcher, options])
useEffect(() => {
fetchData()
}, [fetchData])
return { data, loading, error, refetch: fetchData }
}
// Form hook with validation
interface UseFormOptions<T> {
initialData?: Partial<T>
validate?: (data: T) => ValidationResult<T>
onSubmit?: (data: T) => Promise<void>
}
interface UseFormResult<T> {
data: T
errors: Record<keyof T, string>
isValid: boolean
isSubmitting: boolean
updateField: <K extends keyof T>(field: K, value: T[K]) => void
handleSubmit: () => Promise<void>
reset: () => void
}
function useForm<T extends Record<string, any>>(
defaultData: T,
options: UseFormOptions<T> = {}
): UseFormResult<T> {
const [data, setData] = useState<T>({ ...defaultData, ...options.initialData })
const [errors, setErrors] = useState<Record<keyof T, string>>({} as Record<keyof T, string>)
const [isSubmitting, setIsSubmitting] = useState(false)
const updateField = useCallback(<K extends keyof T>(field: K, value: T[K]) => {
setData(prev => ({ ...prev, [field]: value }))
// Clear error when field is updated
if (errors[field]) {
setErrors(prev => ({ ...prev, [field]: '' }))
}
}, [errors])
const handleSubmit = useCallback(async () => {
if (options.validate) {
const validation = options.validate(data)
if (!validation.isValid) {
setErrors(validation.errors as Record<keyof T, string>)
return
}
}
if (options.onSubmit) {
setIsSubmitting(true)
try {
await options.onSubmit(data)
} catch (error) {
console.error('Form submission error:', error)
} finally {
setIsSubmitting(false)
}
}
}, [data, options])
const reset = useCallback(() => {
setData({ ...defaultData, ...options.initialData })
setErrors({} as Record<keyof T, string>)
setIsSubmitting(false)
}, [defaultData, options.initialData])
const isValid = Object.values(errors).every(error => !error)
return {
data,
errors,
isValid,
isSubmitting,
updateField,
handleSubmit,
reset
}
}
// Usage example
interface UserFormData {
name: string
email: string
age: number
}
function UserForm() {
const {
data,
errors,
isValid,
isSubmitting,
updateField,
handleSubmit,
reset
} = useForm<UserFormData>(
{ name: '', email: '', age: 0 },
{
validate: (data) => {
const errors: Partial<Record<keyof UserFormData, string>> = {}
if (!data.name) errors.name = 'Name is required'
if (!data.email) errors.email = 'Email is required'
if (data.age < 18) errors.age = 'Must be 18 or older'
return {
isValid: Object.keys(errors).length === 0,
errors: errors as Record<keyof UserFormData, string>
}
},
onSubmit: async (data) => {
console.log('Submitting:', data)
// API call here
}
}
)
return (
<form onSubmit={(e) => { e.preventDefault(); handleSubmit() }}>
<input
value={data.name}
onChange={(e) => updateField('name', e.target.value)}
placeholder="Name"
/>
{errors.name && <span className="error">{errors.name}</span>}
<input
value={data.email}
onChange={(e) => updateField('email', e.target.value)}
placeholder="Email"
/>
{errors.email && <span className="error">{errors.email}</span>}
<input
type="number"
value={data.age}
onChange={(e) => updateField('age', parseInt(e.target.value))}
placeholder="Age"
/>
{errors.age && <span className="error">{errors.age}</span>}
<button type="submit" disabled={!isValid || isSubmitting}>
{isSubmitting ? 'Submitting...' : 'Submit'}
</button>
<button type="button" onClick={reset}>
Reset
</button>
</form>
)
}
Next.js Integration
API Routes with Types
Type-safe Next.js API routes.
import type { NextApiRequest, NextApiResponse } from 'next'
import type { ApiResponse, ApiError, PaginatedResponse } from '@foundrykit/types'
// Typed API handler
interface TypedNextApiRequest<T = any> extends Omit<NextApiRequest, 'body'> {
body: T
}
type ApiHandler<TRequest = any, TResponse = any> = (
req: TypedNextApiRequest<TRequest>,
res: NextApiResponse<ApiResponse<TResponse> | ApiError>
) => Promise<void> | void
// Generic API response helper
function createApiResponse<T>(data: T, message?: string): ApiResponse<T> {
return {
data,
success: true,
message,
timestamp: new Date().toISOString()
}
}
function createApiError(code: string, message: string, details?: any): ApiError {
return {
error: true,
code,
message,
details,
timestamp: new Date().toISOString()
}
}
// User API endpoints
interface CreateUserRequest {
name: string
email: string
age: number
}
interface UpdateUserRequest {
name?: string
email?: string
age?: number
}
// GET /api/users
const getUsersHandler: ApiHandler<never, PaginatedResponse<User>> = async (req, res) => {
const { page = 1, limit = 10 } = req.query
try {
const users = await getUsersFromDatabase({
page: Number(page),
limit: Number(limit)
})
const response: PaginatedResponse<User> = {
data: users.data,
success: true,
timestamp: new Date().toISOString(),
pagination: {
page: Number(page),
limit: Number(limit),
total: users.total,
totalPages: Math.ceil(users.total / Number(limit)),
hasNextPage: Number(page) * Number(limit) < users.total,
hasPrevPage: Number(page) > 1
}
}
res.status(200).json(response)
} catch (error) {
res.status(500).json(createApiError('INTERNAL_ERROR', 'Failed to fetch users'))
}
}
// POST /api/users
const createUserHandler: ApiHandler<CreateUserRequest, User> = async (req, res) => {
const { name, email, age } = req.body
// Validation
if (!name || !email || !age) {
return res.status(400).json(
createApiError('VALIDATION_ERROR', 'Missing required fields', {
required: ['name', 'email', 'age']
})
)
}
try {
const user = await createUserInDatabase({ name, email, age })
res.status(201).json(createApiResponse(user, 'User created successfully'))
} catch (error) {
res.status(500).json(createApiError('INTERNAL_ERROR', 'Failed to create user'))
}
}
// Route handler with method switching
export default function handler(req: NextApiRequest, res: NextApiResponse) {
switch (req.method) {
case 'GET':
return getUsersHandler(req, res)
case 'POST':
return createUserHandler(req, res)
default:
res.setHeader('Allow', ['GET', 'POST'])
res.status(405).json(createApiError('METHOD_NOT_ALLOWED', 'Method not allowed'))
}
}
Type-safe Client-side Data Fetching
import type { ApiResponse, PaginatedResponse } from '@foundrykit/types'
// Type-safe API client for Next.js
class NextApiClient {
private baseUrl: string
constructor(baseUrl = '/api') {
this.baseUrl = baseUrl
}
private async request<T>(
endpoint: string,
options: RequestInit = {}
): Promise<T> {
const response = await fetch(`${this.baseUrl}${endpoint}`, {
headers: {
'Content-Type': 'application/json',
...options.headers,
},
...options,
})
if (!response.ok) {
const error = await response.json()
throw error
}
return response.json()
}
async getUsers(params?: {
page?: number
limit?: number
}): Promise<PaginatedResponse<User>> {
const searchParams = new URLSearchParams()
if (params?.page) searchParams.set('page', String(params.page))
if (params?.limit) searchParams.set('limit', String(params.limit))
const query = searchParams.toString()
return this.request<PaginatedResponse<User>>(
`/users${query ? `?${query}` : ''}`
)
}
async createUser(userData: CreateUserRequest): Promise<ApiResponse<User>> {
return this.request<ApiResponse<User>>('/users', {
method: 'POST',
body: JSON.stringify(userData),
})
}
async updateUser(id: string, userData: UpdateUserRequest): Promise<ApiResponse<User>> {
return this.request<ApiResponse<User>>(`/users/${id}`, {
method: 'PUT',
body: JSON.stringify(userData),
})
}
}
// Usage in Next.js pages
import { GetServerSideProps } from 'next'
interface UsersPageProps {
initialUsers: PaginatedResponse<User>
}
export default function UsersPage({ initialUsers }: UsersPageProps) {
const [users, setUsers] = useState(initialUsers)
const apiClient = new NextApiClient()
const loadMore = async () => {
const nextPage = await apiClient.getUsers({
page: users.pagination.page + 1,
limit: users.pagination.limit
})
setUsers(prev => ({
...nextPage,
data: [...prev.data, ...nextPage.data]
}))
}
return (
<div>
{users.data.map(user => (
<div key={user.id}>{user.name}</div>
))}
{users.pagination.hasNextPage && (
<button onClick={loadMore}>Load More</button>
)}
</div>
)
}
export const getServerSideProps: GetServerSideProps<UsersPageProps> = async () => {
const apiClient = new NextApiClient('http://localhost:3000/api')
const initialUsers = await apiClient.getUsers({ page: 1, limit: 10 })
return {
props: {
initialUsers
}
}
}
Express.js Integration
Typed Express Routes
Create type-safe Express.js routes.
import express from 'express'
import type { Request, Response, NextFunction } from 'express'
import type { ApiResponse, ApiError } from '@foundrykit/types'
// Typed Express request/response
interface TypedRequest<TBody = any, TQuery = any, TParams = any> extends Request {
body: TBody
query: TQuery
params: TParams
}
interface TypedResponse<T = any> extends Response {
json: (body: ApiResponse<T> | ApiError) => TypedResponse<T>
}
type TypedRequestHandler<TBody = any, TResponse = any, TQuery = any, TParams = any> = (
req: TypedRequest<TBody, TQuery, TParams>,
res: TypedResponse<TResponse>,
next: NextFunction
) => void | Promise<void>
// Response helpers
function successResponse<T>(data: T, message?: string): ApiResponse<T> {
return {
data,
success: true,
message,
timestamp: new Date().toISOString()
}
}
function errorResponse(code: string, message: string, details?: any): ApiError {
return {
error: true,
code,
message,
details,
timestamp: new Date().toISOString()
}
}
// User routes with full type safety
interface GetUsersQuery {
page?: string
limit?: string
search?: string
}
interface GetUserParams {
id: string
}
interface CreateUserBody {
name: string
email: string
age: number
}
interface UpdateUserBody {
name?: string
email?: string
age?: number
}
// GET /users
const getUsers: TypedRequestHandler<never, User[], GetUsersQuery> = async (req, res) => {
try {
const page = parseInt(req.query.page || '1')
const limit = parseInt(req.query.limit || '10')
const search = req.query.search
const users = await getUsersFromDatabase({ page, limit, search })
res.json(successResponse(users))
} catch (error) {
res.status(500).json(errorResponse('INTERNAL_ERROR', 'Failed to fetch users'))
}
}
// GET /users/:id
const getUser: TypedRequestHandler<never, User, never, GetUserParams> = async (req, res) => {
try {
const { id } = req.params
const user = await getUserFromDatabase(id)
if (!user) {
return res.status(404).json(
errorResponse('NOT_FOUND', 'User not found', { id })
)
}
res.json(successResponse(user))
} catch (error) {
res.status(500).json(errorResponse('INTERNAL_ERROR', 'Failed to fetch user'))
}
}
// POST /users
const createUser: TypedRequestHandler<CreateUserBody, User> = async (req, res) => {
try {
const { name, email, age } = req.body
// Validation
if (!name || !email || !age) {
return res.status(400).json(
errorResponse('VALIDATION_ERROR', 'Missing required fields', {
required: ['name', 'email', 'age']
})
)
}
const user = await createUserInDatabase({ name, email, age })
res.status(201).json(successResponse(user, 'User created successfully'))
} catch (error) {
res.status(500).json(errorResponse('INTERNAL_ERROR', 'Failed to create user'))
}
}
// PUT /users/:id
const updateUser: TypedRequestHandler<UpdateUserBody, User, never, GetUserParams> = async (req, res) => {
try {
const { id } = req.params
const updateData = req.body
const user = await updateUserInDatabase(id, updateData)
if (!user) {
return res.status(404).json(
errorResponse('NOT_FOUND', 'User not found', { id })
)
}
res.json(successResponse(user, 'User updated successfully'))
} catch (error) {
res.status(500).json(errorResponse('INTERNAL_ERROR', 'Failed to update user'))
}
}
// Express router with typed routes
const userRouter = express.Router()
userRouter.get('/', getUsers)
userRouter.get('/:id', getUser)
userRouter.post('/', createUser)
userRouter.put('/:id', updateUser)
export default userRouter
GraphQL Integration
Type-safe GraphQL with FoundryKit Types
import { buildSchema, GraphQLResolveInfo } from 'graphql'
import type { ApiResponse, PaginatedResponse } from '@foundrykit/types'
// GraphQL context with types
interface GraphQLContext {
user?: User
apiClient: ApiClient
}
// Resolver types
type GraphQLResolver<TArgs = any, TResult = any> = (
parent: any,
args: TArgs,
context: GraphQLContext,
info: GraphQLResolveInfo
) => Promise<TResult> | TResult
// Query argument types
interface GetUsersArgs {
page?: number
limit?: number
search?: string
}
interface GetUserArgs {
id: string
}
interface CreateUserArgs {
input: CreateUserInput
}
interface CreateUserInput {
name: string
email: string
age: number
}
// GraphQL resolvers with full type safety
const resolvers = {
Query: {
users: (async (_, args: GetUsersArgs, context) => {
const { page = 1, limit = 10, search } = args
try {
const response = await context.apiClient.getUsers({ page, limit, search })
return {
data: response.data,
pagination: response.pagination
}
} catch (error) {
throw new Error('Failed to fetch users')
}
}) as GraphQLResolver<GetUsersArgs, PaginatedResponse<User>>,
user: (async (_, args: GetUserArgs, context) => {
try {
const response = await context.apiClient.getUser(args.id)
return response.data
} catch (error) {
throw new Error('Failed to fetch user')
}
}) as GraphQLResolver<GetUserArgs, User>
},
Mutation: {
createUser: (async (_, args: CreateUserArgs, context) => {
try {
const response = await context.apiClient.createUser(args.input)
return response.data
} catch (error) {
throw new Error('Failed to create user')
}
}) as GraphQLResolver<CreateUserArgs, User>
}
}
// GraphQL schema
const schema = buildSchema(`
type User {
id: ID!
name: String!
email: String!
age: Int!
}
type Pagination {
page: Int!
limit: Int!
total: Int!
totalPages: Int!
hasNextPage: Boolean!
hasPrevPage: Boolean!
}
type UserConnection {
data: [User!]!
pagination: Pagination!
}
input CreateUserInput {
name: String!
email: String!
age: Int!
}
type Query {
users(page: Int, limit: Int, search: String): UserConnection!
user(id: ID!): User
}
type Mutation {
createUser(input: CreateUserInput!): User!
}
`)
// Type-safe GraphQL client
interface GraphQLResponse<T> {
data?: T
errors?: Array<{ message: string; path?: string[] }>
}
class TypedGraphQLClient {
constructor(private endpoint: string) {}
private async request<T>(query: string, variables?: any): Promise<T> {
const response = await fetch(this.endpoint, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ query, variables })
})
const result: GraphQLResponse<T> = await response.json()
if (result.errors) {
throw new Error(result.errors[0].message)
}
return result.data!
}
async getUsers(variables: GetUsersArgs): Promise<{ users: PaginatedResponse<User> }> {
const query = `
query GetUsers($page: Int, $limit: Int, $search: String) {
users(page: $page, limit: $limit, search: $search) {
data {
id
name
email
age
}
pagination {
page
limit
total
totalPages
hasNextPage
hasPrevPage
}
}
}
`
return this.request(query, variables)
}
async createUser(variables: CreateUserArgs): Promise<{ createUser: User }> {
const query = `
mutation CreateUser($input: CreateUserInput!) {
createUser(input: $input) {
id
name
email
age
}
}
`
return this.request(query, variables)
}
}
Database Integration (Prisma)
Type-safe Database Operations
import { PrismaClient } from '@prisma/client'
import type { ApiResponse, PaginatedResponse } from '@foundrykit/types'
const prisma = new PrismaClient()
// Repository pattern with FoundryKit types
interface UserRepository {
findMany(params: {
page: number
limit: number
search?: string
}): Promise<PaginatedResponse<User>>
findById(id: string): Promise<User | null>
create(data: CreateUserRequest): Promise<User>
update(id: string, data: UpdateUserRequest): Promise<User | null>
delete(id: string): Promise<boolean>
}
class PrismaUserRepository implements UserRepository {
async findMany({ page, limit, search }): Promise<PaginatedResponse<User>> {
const skip = (page - 1) * limit
const where = search
? {
OR: [
{ name: { contains: search, mode: 'insensitive' as const } },
{ email: { contains: search, mode: 'insensitive' as const } }
]
}
: {}
const [users, total] = await Promise.all([
prisma.user.findMany({
where,
skip,
take: limit,
orderBy: { createdAt: 'desc' }
}),
prisma.user.count({ where })
])
return {
data: users,
success: true,
timestamp: new Date().toISOString(),
pagination: {
page,
limit,
total,
totalPages: Math.ceil(total / limit),
hasNextPage: page * limit < total,
hasPrevPage: page > 1
}
}
}
async findById(id: string): Promise<User | null> {
return prisma.user.findUnique({
where: { id }
})
}
async create(data: CreateUserRequest): Promise<User> {
return prisma.user.create({
data: {
name: data.name,
email: data.email,
age: data.age
}
})
}
async update(id: string, data: UpdateUserRequest): Promise<User | null> {
try {
return await prisma.user.update({
where: { id },
data: {
...(data.name && { name: data.name }),
...(data.email && { email: data.email }),
...(data.age && { age: data.age })
}
})
} catch (error) {
return null
}
}
async delete(id: string): Promise<boolean> {
try {
await prisma.user.delete({
where: { id }
})
return true
} catch (error) {
return false
}
}
}
// Service layer with type safety
class UserService {
constructor(private repository: UserRepository) {}
async getUsers(params: {
page?: number
limit?: number
search?: string
}): Promise<PaginatedResponse<User>> {
const { page = 1, limit = 10, search } = params
return this.repository.findMany({ page, limit, search })
}
async getUser(id: string): Promise<ApiResponse<User | null>> {
const user = await this.repository.findById(id)
return {
data: user,
success: true,
timestamp: new Date().toISOString(),
message: user ? 'User found' : 'User not found'
}
}
async createUser(data: CreateUserRequest): Promise<ApiResponse<User>> {
const user = await this.repository.create(data)
return {
data: user,
success: true,
timestamp: new Date().toISOString(),
message: 'User created successfully'
}
}
async updateUser(id: string, data: UpdateUserRequest): Promise<ApiResponse<User | null>> {
const user = await this.repository.update(id, data)
return {
data: user,
success: true,
timestamp: new Date().toISOString(),
message: user ? 'User updated successfully' : 'User not found'
}
}
async deleteUser(id: string): Promise<ApiResponse<boolean>> {
const success = await this.repository.delete(id)
return {
data: success,
success,
timestamp: new Date().toISOString(),
message: success ? 'User deleted successfully' : 'Failed to delete user'
}
}
}
// Usage
const userRepository = new PrismaUserRepository()
const userService = new UserService(userRepository)
export { userService }
Testing Integration
Type-safe Testing with Jest/Vitest
import { describe, it, expect, vi, beforeEach } from 'vitest'
import type { ApiResponse, ApiError, ValidationResult } from '@foundrykit/types'
// Mock API responses with types
const createMockApiResponse = <T>(data: T): ApiResponse<T> => ({
data,
success: true,
timestamp: new Date().toISOString()
})
const createMockApiError = (code: string, message: string): ApiError => ({
error: true,
code,
message,
timestamp: new Date().toISOString()
})
// Test utilities
interface TestUser {
id: string
name: string
email: string
age: number
}
const createTestUser = (overrides: Partial<TestUser> = {}): TestUser => ({
id: '1',
name: 'John Doe',
email: 'john@example.com',
age: 30,
...overrides
})
// Component testing with types
import { render, screen, fireEvent, waitFor } from '@testing-library/react'
import { UserForm } from './UserForm'
describe('UserForm', () => {
const mockOnSubmit = vi.fn<[TestUser], Promise<void>>()
beforeEach(() => {
mockOnSubmit.mockClear()
})
it('should submit form with valid data', async () => {
const testUser = createTestUser()
render(<UserForm onSubmit={mockOnSubmit} />)
fireEvent.change(screen.getByLabelText(/name/i), {
target: { value: testUser.name }
})
fireEvent.change(screen.getByLabelText(/email/i), {
target: { value: testUser.email }
})
fireEvent.change(screen.getByLabelText(/age/i), {
target: { value: String(testUser.age) }
})
fireEvent.click(screen.getByRole('button', { name: /submit/i }))
await waitFor(() => {
expect(mockOnSubmit).toHaveBeenCalledWith({
name: testUser.name,
email: testUser.email,
age: testUser.age
})
})
})
it('should show validation errors for invalid data', async () => {
render(<UserForm onSubmit={mockOnSubmit} />)
fireEvent.click(screen.getByRole('button', { name: /submit/i }))
await waitFor(() => {
expect(screen.getByText(/name is required/i)).toBeInTheDocument()
expect(screen.getByText(/email is required/i)).toBeInTheDocument()
})
expect(mockOnSubmit).not.toHaveBeenCalled()
})
})
// API testing with types
describe('UserAPI', () => {
it('should fetch users successfully', async () => {
const mockUsers = [createTestUser(), createTestUser({ id: '2', name: 'Jane Doe' })]
const mockResponse = createMockApiResponse(mockUsers)
global.fetch = vi.fn().mockResolvedValueOnce({
ok: true,
json: () => Promise.resolve(mockResponse)
})
const apiClient = new UserApiClient()
const result = await apiClient.getUsers()
expect(result.data).toEqual(mockUsers)
expect(result.success).toBe(true)
})
it('should handle API errors', async () => {
const mockError = createMockApiError('NOT_FOUND', 'Users not found')
global.fetch = vi.fn().mockResolvedValueOnce({
ok: false,
json: () => Promise.resolve(mockError)
})
const apiClient = new UserApiClient()
await expect(apiClient.getUsers()).rejects.toEqual(mockError)
})
})
// Validation testing
describe('UserValidation', () => {
it('should validate user data correctly', () => {
const validUser = createTestUser()
const result = validateUser(validUser)
expect(result.isValid).toBe(true)
expect(Object.keys(result.errors)).toHaveLength(0)
})
it('should return errors for invalid user data', () => {
const invalidUser = createTestUser({ name: '', email: 'invalid-email', age: 15 })
const result = validateUser(invalidUser)
expect(result.isValid).toBe(false)
expect(result.errors.name).toBeTruthy()
expect(result.errors.email).toBeTruthy()
expect(result.errors.age).toBeTruthy()
})
})
// Type guard testing
describe('Type Guards', () => {
it('should correctly identify valid user objects', () => {
const validUser = createTestUser()
expect(isUser(validUser)).toBe(true)
})
it('should reject invalid user objects', () => {
const invalidUser = { id: 1, name: 'John' } // missing email, age; wrong id type
expect(isUser(invalidUser)).toBe(false)
})
})
Next Steps
- Check out best practices for optimal type usage
- Explore the type definitions reference for detailed type documentation
- Learn about custom type patterns for advanced usage