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:
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 (),
});
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:
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:
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:
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:
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:
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
}
}
The SDK is fully typed for all JavaScript runtimes:
Node.js
Bun
Deno
Cloudflare Workers
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 );
import { Paystack } from '@efobi/paystack' ;
import type { TxnVerifySuccess } from '@efobi/paystack/transaction' ;
const paystack = new Paystack ( Bun . env . PAYSTACK_SECRET_KEY ! );
const result = await paystack . transaction . verify ( 'ref_123' );
if ( result . data ) {
const verified : TxnVerifySuccess = result . data ;
}
import { Paystack } from '@efobi/paystack' ;
import type { Customer } from '@efobi/paystack' ;
const paystack = new Paystack ( Deno . env . get ( 'PAYSTACK_SECRET_KEY' ) ! );
const result = await paystack . transaction . verify ( 'ref_123' );
if ( result . data ) {
const customer : Customer = result . data . data . customer ;
}
import { Paystack } from '@efobi/paystack' ;
export default {
async fetch ( request : Request , env : Env ) {
const paystack = new Paystack ( env . PAYSTACK_SECRET_KEY );
const result = await paystack . transaction . list ();
return Response . json ( result . data );
}
} ;
TypeScript Configuration
Recommended tsconfig.json for best experience:
{
"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