Skip to main content
The Paystack TypeScript SDK provides comprehensive type safety through a combination of TypeScript’s compile-time checking and Zod’s runtime validation. This dual-layer approach ensures your code is both type-safe and validates data at runtime.

Why Type Safety Matters

Type safety prevents entire classes of bugs before they reach production:

Catch Errors Early

TypeScript catches type mismatches during development, not in production.

Better IntelliSense

Your IDE provides accurate autocomplete and inline documentation.

Refactor Safely

Change APIs with confidence knowing TypeScript will flag breaking changes.

Runtime Validation

Zod validates data at runtime, protecting against invalid API responses.

TypeScript Benefits

Full Type Coverage

Every SDK method has complete TypeScript definitions:
import { Paystack } from '@efobi/paystack';

const paystack = new Paystack('sk_test_...');

// TypeScript knows the exact shape of input and output
const result = await paystack.transaction.initialize({
  email: 'customer@example.com',  // string (validated as email)
  amount: '50000',                 // string (required)
  currency: 'NGN',                 // 'NGN' | 'USD' | 'GHS' | 'ZAR' | 'KES' | 'XOF'
  reference: 'ref_123',            // string (optional)
  callback_url: 'https://...',     // string (validated as URL, optional)
  metadata: { userId: 123 }        // any (optional)
});

// TypeScript knows result has 'data' and 'error' properties
if (result.data) {
  const url = result.data.data.authorization_url; // string
  const code = result.data.data.access_code;      // string
  const ref = result.data.data.reference;         // string
}

IntelliSense Support

Your IDE provides intelligent code completion:
paystack.transaction.  // IDE shows: initialize, verify, list, getTransactionById, etc.

paystack.transaction.initialize({
  email: '',  // IDE suggests: string (email format)
  amount: '', // IDE suggests: string (required)
  // Press Ctrl+Space to see all available options
});
Hover over any method or property in your IDE to see detailed documentation extracted from JSDoc comments.

Compile-Time Error Detection

TypeScript catches mistakes before runtime:
// ❌ TypeScript Error: Type 'number' is not assignable to type 'string'
const result = await paystack.transaction.initialize({
  email: 'customer@example.com',
  amount: 50000  // Should be a string: '50000'
});

// ❌ TypeScript Error: Property 'invalid' does not exist
const result = await paystack.transaction.initialize({
  email: 'customer@example.com',
  amount: '50000',
  invalid: true  // Unknown property
});

// ✅ Correct
const result = await paystack.transaction.initialize({
  email: 'customer@example.com',
  amount: '50000'
});

Zod Runtime Validation

While TypeScript provides compile-time safety, Zod validates data at runtime. This is crucial for:
  • API responses that might not match expected schemas
  • User input that bypasses TypeScript
  • Third-party data from webhooks or external sources

How Zod Works

Zod schemas define the shape and validation rules for data. Here’s an example from src/zod/transaction.ts:15-85:
src/zod/transaction.ts
export const txnInitializeInput = z.object({
  amount: z.string(),
  email: z.email(),
  currency: z.optional(currency),
  reference: z.string().optional(),
  callback_url: z.url().optional(),
  plan: z.string().optional(),
  invoice_limit: z.number().optional(),
  metadata: z.any().optional(),
  channels: z
    .array(
      z.enum([
        "card",
        "bank",
        "apple_pay",
        "ussd",
        "qr",
        "mobile_money",
        "bank_transfer",
        "eft",
      ])
    )
    .optional(),
  split_code: z.string().optional(),
  subaccount: z.string().optional(),
  transaction_charge: z.string().optional(),
  bearer: z.enum(["account", "subaccount"]).optional(),
});

Input Validation

Zod automatically validates all inputs before API requests:
const result = await paystack.transaction.initialize({
  email: 'not-an-email',  // ❌ Will fail Zod validation
  amount: '50000'
});

if (result.error) {
  console.log(result.error.issues);
  // [
  //   {
  //     validation: 'email',
  //     code: 'invalid_string',
  //     message: 'Invalid email',
  //     path: ['email']
  //   }
  // ]
}

Output Validation

Zod also validates API responses to ensure they match the expected schema:
src/main/transaction.ts
if (!response.ok) {
  const { data, error } = await genericResponse.safeParseAsync(raw);
  return { data, error };
}
const { data, error } = await txnInitializeSuccess.safeParseAsync(raw);
return { data, error };
This protects your application from unexpected API changes or malformed responses.

Type Inference

Zod schemas can be converted to TypeScript types using z.infer. From src/zod/transaction.ts:1503-1517:
src/zod/transaction.ts
export type TxnInitializeInput = z.infer<typeof txnInitializeInput>;
export type TxnInitializeSuccess = z.infer<typeof txnInitializeSuccess>;
export type TransactionShared = z.infer<typeof transactionShared>;
export type TxnVerifySuccess = z.infer<typeof txnVerifySuccess>;
export type TxnListInput = z.infer<typeof txnListInput>;
export type TxnSingleSuccess = z.infer<typeof txnSingleSuccess>;
export type TxnListSuccess = z.infer<typeof txnListSuccess>;
export type TxnChargeInput = z.infer<typeof txnChargeInput>;
export type TxnChargeSuccess = z.infer<typeof txnChargeSuccess>;
export type TxnTimelineSuccess = z.infer<typeof txnTimelineSuccess>;
export type TxnTotalsSuccess = z.infer<typeof txnTotalsSuccess>;
export type TxnExportInput = z.infer<typeof txnExportInput>;
export type TxnExportSuccess = z.infer<typeof txnExportSuccess>;
export type TxnPartialDebitInput = z.infer<typeof txnPartialDebitInput>;
export type TxnPartialDebitSuccess = z.infer<typeof txnPartialDebitSuccess>;
This ensures TypeScript types and runtime validation stay in sync.

Importing Types

You can import types for use in your application:
import type { 
  TxnInitializeInput,
  TxnInitializeSuccess,
  TxnVerifySuccess,
  TransactionShared
} from '@efobi/paystack/transaction';

// Use types in your code
function createTransaction(data: TxnInitializeInput) {
  return paystack.transaction.initialize(data);
}

function handleSuccess(response: TxnInitializeSuccess) {
  const { authorization_url, access_code, reference } = response.data;
  // TypeScript knows these properties exist
}

Common Types

The SDK exports many reusable types from src/zod/index.ts:228-237:
src/zod/index.ts
export type Meta = z.infer<typeof meta>;
export type GenericResponse = z.infer<typeof genericResponse>;
export type Currency = z.infer<typeof currency>;
export type History = z.infer<typeof history>;
export type Log = z.infer<typeof log>;
export type Authorization = z.infer<typeof authorization>;
export type Customer = z.infer<typeof customer>;
export type GenericInput = z.infer<typeof genericInput>;
export type Subaccount = z.infer<typeof subaccount>;
export type Plan = z.infer<typeof plan>;
Import and use these in your application:
import type { Currency, Customer, Authorization } from '@efobi/paystack';

interface PaymentData {
  currency: Currency;        // 'NGN' | 'USD' | 'GHS' | 'ZAR' | 'KES' | 'XOF'
  customer: Customer;        // Full customer object
  authorization: Authorization; // Card authorization details
}

Validation Schemas

Currency Validation

The SDK validates currency codes from src/zod/index.ts:40-49:
src/zod/index.ts
export const currency = z.enum(["NGN", "USD", "GHS", "ZAR", "KES", "XOF"]).default("NGN");
Usage:
const result = await paystack.transaction.initialize({
  email: 'customer@example.com',
  amount: '50000',
  currency: 'NGN'  // ✅ Valid
  // currency: 'EUR'  // ❌ TypeScript error: Type '"EUR"' is not assignable to type Currency
});

Email Validation

Emails are validated using Zod’s built-in email validator:
email: z.email()  // Validates RFC 5322 email format
// ✅ Valid emails
const emails = [
  'user@example.com',
  'first.last@company.co.uk',
  'name+tag@domain.com'
];

// ❌ Invalid emails
const invalid = [
  'not-an-email',
  '@example.com',
  'user@',
  'user @example.com'
];

URL Validation

Callback URLs are validated as proper URLs:
callback_url: z.url().optional()
// ✅ Valid URLs
const urls = [
  'https://example.com/callback',
  'https://app.example.com/api/webhook',
  'http://localhost:3000/callback'
];

// ❌ Invalid URLs
const invalid = [
  'not-a-url',
  'example.com',  // Missing protocol
  '/callback'     // Relative URL
];

Custom Object Validation

From src/zod/index.ts:127-147, the SDK validates complex nested objects:
src/zod/index.ts
export const customer = z.object({
  id: z.number(),
  first_name: z.nullable(z.string()),
  last_name: z.nullable(z.string()),
  email: z.email(),
  phone: z.nullable(z.string()),
  metadata: z.nullable(z.any()),
  customer_code: z.string(),
  risk_action: z.string(),
  international_format_phone: z.nullable(z.string()),
});

Advanced Type Patterns

Discriminated Unions

Handle different response types safely:
type PaystackResult<T> = 
  | { data: T; error: undefined }
  | { data: undefined; error: ZodError };

function processResult<T>(result: PaystackResult<T>): T {
  if (result.error) {
    throw new Error('Validation failed');
  }
  return result.data;  // TypeScript knows this is T, not undefined
}

Generic Helpers

Create type-safe wrappers:
import type { Paystack } from '@efobi/paystack';

type ExtractData<T> = T extends { data: infer D } ? D : never;

type TransactionData = ExtractData<
  Awaited<ReturnType<Paystack['transaction']['initialize']>>
>;
// TransactionData is the success response type

Utility Types

Use TypeScript utility types with SDK types:
import type { TxnInitializeInput } from '@efobi/paystack/transaction';

// Make all fields required
type RequiredTransaction = Required<TxnInitializeInput>;

// Pick specific fields
type MinimalTransaction = Pick<TxnInitializeInput, 'email' | 'amount'>;

// Omit fields
type TransactionWithoutMetadata = Omit<TxnInitializeInput, 'metadata'>;

// Make all fields optional
type PartialTransaction = Partial<TxnInitializeInput>;

Schema Composition

Zod schemas can be composed and extended:
import { z } from 'zod';
import type { TxnInitializeInput } from '@efobi/paystack/transaction';

// Extend the base schema
const customTransactionInput = z.object({
  // Add your custom fields
  userId: z.string(),
  sessionId: z.string().optional(),
});

type CustomTransaction = TxnInitializeInput & z.infer<typeof customTransactionInput>;

function initializeWithTracking(data: CustomTransaction) {
  const { userId, sessionId, ...paystackData } = data;
  
  // Track in your system
  trackTransaction(userId, sessionId);
  
  // Initialize with Paystack
  return paystack.transaction.initialize(paystackData);
}

Safe Parsing

Manually validate data using Zod schemas:
import { z } from 'zod';

const userInput = getUserInput(); // Unknown data

const schema = z.object({
  email: z.email(),
  amount: z.string()
});

const result = schema.safeParse(userInput);

if (result.success) {
  // Data is validated and typed
  await paystack.transaction.initialize(result.data);
} else {
  // Handle validation errors
  console.error(result.error.issues);
}

Best Practices

Trust the Types

Let TypeScript guide you. If something doesn’t type-check, there’s usually a good reason.
// Trust TypeScript's error messages
const result = await paystack.transaction.initialize({
  email: 123  // Error helps you fix the issue
});

Import Types

Import types for reusability and consistency across your codebase.
import type { TxnInitializeInput } from '@efobi/paystack/transaction';

function prepare(data: TxnInitializeInput) {
  // Type-safe function
}

Handle Validation Errors

Always check for validation errors in the result object.
if (result.error) {
  // Zod found validation issues
  const errors = result.error.flatten();
}

Use Strict Mode

Enable strict TypeScript settings in tsconfig.json:
{
  "compilerOptions": {
    "strict": true,
    "strictNullChecks": true
  }
}

Platform Support

The SDK is fully typed for all JavaScript runtimes:
import { Paystack } from '@efobi/paystack';
import type { TxnInitializeInput } from '@efobi/paystack/transaction';

const paystack = new Paystack(process.env.PAYSTACK_SECRET_KEY!);

const data: TxnInitializeInput = {
  email: 'customer@example.com',
  amount: '50000'
};

const result = await paystack.transaction.initialize(data);

TypeScript Configuration

Recommended tsconfig.json for best experience:
tsconfig.json
{
  "compilerOptions": {
    "target": "ES2020",
    "module": "ESNext",
    "lib": ["ES2020"],
    "moduleResolution": "bundler",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,
    "resolveJsonModule": true,
    "isolatedModules": true,
    "strictNullChecks": true,
    "noUncheckedIndexedAccess": true
  }
}

Next Steps