TypeScript Integration
Build type-safe SMS applications with TypeScript and comprehensive type definitions.
Quick Setup
Create a TypeScript project with full type safety for SMS development.
1. Install Dependencies
# Initialize TypeScript project npm init -y npm install typescript @types/node ts-node # Install SMS dependencies npm install axios @types/axios # Install sms-dev globally npm install -g @relay/sms-dev # Initialize TypeScript config npx tsc --init # Start sms-dev sms-dev
Type Definitions
Core SMS Types
Essential TypeScript interfaces for SMS development
// types/sms.ts export interface SMSMessage { id: string; to: string; from: string; body: string; status: MessageStatus; timestamp: string; direction?: 'inbound' | 'outbound'; } export type MessageStatus = 'queued' | 'sent' | 'delivered' | 'failed'; export interface SendSMSRequest { to: string; from: string; body: string; } export interface SendSMSResponse { id: string; status: MessageStatus; message?: string; } export interface SMSError { error: string; code?: string; details?: unknown; } export interface Conversation { phoneNumber: string; messages: SMSMessage[]; lastActivity: string; unreadCount: number; } export interface WebhookPayload { messageId: string; to: string; from: string; body: string; timestamp: string; } export interface SMSClientConfig { baseUrl?: string; timeout?: number; retries?: number; }
Advanced Types
Generic types and utility types for complex workflows
// types/advanced.ts export type SMSResult<T = SMSMessage> = { success: true; data: T; } | { success: false; error: SMSError; }; export interface ConversationFlow<T = Record<string, unknown>> { id: string; name: string; steps: FlowStep<T>[]; context?: T; } export interface FlowStep<T = Record<string, unknown>> { id: string; type: 'message' | 'condition' | 'wait' | 'webhook'; config: StepConfig<T>; } export type StepConfig<T> = | MessageStepConfig<T> | ConditionStepConfig<T> | WaitStepConfig | WebhookStepConfig<T>; export interface MessageStepConfig<T> { template: string; variables?: Array<keyof T>; } export interface ConditionStepConfig<T> { condition: (context: T, input: string) => boolean; truePath: string; falsePath: string; } export interface WaitStepConfig { duration: number; unit: 'seconds' | 'minutes' | 'hours'; } export interface WebhookStepConfig<T> { url: string; method: 'GET' | 'POST' | 'PUT'; payload?: Partial<T>; } // Utility types export type PhoneNumber = `+${string}`; export type MessageTemplate<T> = (context: T) => string; export type EventHandler<T> = (event: T) => void | Promise<void>;
Type-Safe SMS Client
SMS Client Implementation
Type-safe SMS client with error handling and validation
// client/SMSClient.ts import axios, { AxiosInstance, AxiosError } from 'axios'; import { SMSMessage, SendSMSRequest, SendSMSResponse, SMSError, SMSResult, SMSClientConfig, PhoneNumber } from '../types/sms'; export class SMSClient { private client: AxiosInstance; private config: Required<SMSClientConfig>; constructor(config: SMSClientConfig = {}) { this.config = { baseUrl: config.baseUrl || 'http://localhost:4001', timeout: config.timeout || 5000, retries: config.retries || 3, }; this.client = axios.create({ baseURL: this.config.baseUrl, timeout: this.config.timeout, headers: { 'Content-Type': 'application/json', }, }); } async sendMessage( to: PhoneNumber, from: PhoneNumber, body: string ): Promise<SMSResult<SMSMessage>> { try { this.validatePhoneNumber(to); this.validatePhoneNumber(from); this.validateMessageBody(body); const request: SendSMSRequest = { to, from, body }; const response = await this.client.post<SendSMSResponse>( '/v1/messages', request ); const message: SMSMessage = { id: response.data.id, to, from, body, status: response.data.status, timestamp: new Date().toISOString(), direction: 'outbound', }; return { success: true, data: message, }; } catch (error) { return { success: false, error: this.handleError(error), }; } } async getMessage(messageId: string): Promise<SMSResult<SMSMessage>> { try { const response = await this.client.get<SMSMessage>( `/v1/messages/${messageId}` ); return { success: true, data: response.data, }; } catch (error) { return { success: false, error: this.handleError(error), }; } } async getMessages( options: { limit?: number; offset?: number; phone?: PhoneNumber; status?: MessageStatus; } = {} ): Promise<SMSResult<SMSMessage[]>> { try { const params = new URLSearchParams(); if (options.limit) params.append('limit', options.limit.toString()); if (options.offset) params.append('offset', options.offset.toString()); if (options.phone) params.append('phone', options.phone); if (options.status) params.append('status', options.status); const response = await this.client.get<{ messages: SMSMessage[] }>( `/v1/messages?${params.toString()}` ); return { success: true, data: response.data.messages, }; } catch (error) { return { success: false, error: this.handleError(error), }; } } private validatePhoneNumber(phone: string): asserts phone is PhoneNumber { if (!phone.startsWith('+') || phone.length < 10) { throw new Error(`Invalid phone number format: ${phone}`); } } private validateMessageBody(body: string): void { if (!body || body.trim().length === 0) { throw new Error('Message body cannot be empty'); } if (body.length > 160) { throw new Error('Message body cannot exceed 160 characters'); } } private handleError(error: unknown): SMSError { if (axios.isAxiosError(error)) { const axiosError = error as AxiosError<SMSError>; return { error: axiosError.response?.data?.error || axiosError.message, code: axiosError.code, details: axiosError.response?.data, }; } if (error instanceof Error) { return { error: error.message, }; } return { error: 'Unknown error occurred', details: error, }; } }
Conversation Manager
Type-safe conversation management with generic context
// managers/ConversationManager.ts import { SMSMessage, Conversation, PhoneNumber, EventHandler, MessageTemplate } from '../types/sms'; import { SMSClient } from '../client/SMSClient'; export class ConversationManager<T = Record<string, unknown>> { private conversations = new Map<PhoneNumber, Conversation>(); private context = new Map<PhoneNumber, T>(); private messageHandlers: EventHandler<SMSMessage>[] = []; constructor(private smsClient: SMSClient) {} async startConversation( phoneNumber: PhoneNumber, initialContext?: T ): Promise<void> { const conversation: Conversation = { phoneNumber, messages: [], lastActivity: new Date().toISOString(), unreadCount: 0, }; this.conversations.set(phoneNumber, conversation); if (initialContext) { this.context.set(phoneNumber, initialContext); } } async sendMessage( phoneNumber: PhoneNumber, template: string | MessageTemplate<T>, from: PhoneNumber = '+1987654321' as PhoneNumber ): Promise<void> { const conversation = this.conversations.get(phoneNumber); if (!conversation) { throw new Error(`No conversation found for ${phoneNumber}`); } const context = this.context.get(phoneNumber); const body = typeof template === 'function' ? template(context as T) : this.interpolateTemplate(template, context); const result = await this.smsClient.sendMessage(phoneNumber, from, body); if (result.success) { conversation.messages.push(result.data); conversation.lastActivity = result.data.timestamp; this.notifyHandlers(result.data); } else { throw new Error(`Failed to send message: ${result.error.error}`); } } receiveMessage(message: SMSMessage): void { const conversation = this.conversations.get(message.from as PhoneNumber); if (conversation) { const inboundMessage: SMSMessage = { ...message, direction: 'inbound', }; conversation.messages.push(inboundMessage); conversation.lastActivity = message.timestamp; conversation.unreadCount++; this.notifyHandlers(inboundMessage); } } getConversation(phoneNumber: PhoneNumber): Conversation | undefined { return this.conversations.get(phoneNumber); } getAllConversations(): Conversation[] { return Array.from(this.conversations.values()); } updateContext(phoneNumber: PhoneNumber, updates: Partial<T>): void { const currentContext = this.context.get(phoneNumber) || {} as T; this.context.set(phoneNumber, { ...currentContext, ...updates }); } getContext(phoneNumber: PhoneNumber): T | undefined { return this.context.get(phoneNumber); } onMessage(handler: EventHandler<SMSMessage>): void { this.messageHandlers.push(handler); } private interpolateTemplate(template: string, context?: T): string { if (!context) return template; return template.replace(/\{\{(\w+)\}\}/g, (match, key) => { const value = (context as Record<string, unknown>)[key]; return value !== undefined ? String(value) : match; }); } private notifyHandlers(message: SMSMessage): void { this.messageHandlers.forEach(handler => { try { handler(message); } catch (error) { console.error('Error in message handler:', error); } }); } }
Usage Examples
Basic Usage
Simple examples of type-safe SMS operations
// examples/basic.ts import { SMSClient } from './client/SMSClient'; import { PhoneNumber } from './types/sms'; async function basicExample() { const client = new SMSClient({ baseUrl: 'http://localhost:4001', timeout: 10000, }); // Type-safe phone numbers const userPhone: PhoneNumber = '+1234567890'; const servicePhone: PhoneNumber = '+1987654321'; // Send a message with full type safety const result = await client.sendMessage( userPhone, servicePhone, 'Welcome to our service! Reply HELP for commands.' ); if (result.success) { console.log('Message sent:', result.data.id); console.log('Status:', result.data.status); } else { console.error('Failed to send:', result.error.error); } // Get message history with filtering const messagesResult = await client.getMessages({ phone: userPhone, limit: 10, }); if (messagesResult.success) { messagesResult.data.forEach(message => { console.log(`${message.direction}: ${message.body}`); }); } } // Type-safe error handling function handleSMSError(error: SMSError): void { switch (error.code) { case 'INVALID_PHONE': console.error('Invalid phone number format'); break; case 'MESSAGE_TOO_LONG': console.error('Message exceeds character limit'); break; default: console.error('SMS error:', error.error); } }
Advanced Usage
Complex workflows with conversation management and context
// examples/advanced.ts import { ConversationManager } from './managers/ConversationManager'; import { SMSClient } from './client/SMSClient'; import { PhoneNumber } from './types/sms'; interface UserContext { name: string; step: 'greeting' | 'menu' | 'support' | 'feedback'; orderNumber?: string; } async function advancedExample() { const client = new SMSClient(); const conversations = new ConversationManager<UserContext>(client); // Set up message handling conversations.onMessage(async (message) => { console.log(`Received: ${message.body} from ${message.from}`); await handleIncomingMessage(conversations, message); }); // Start a conversation with context const userPhone: PhoneNumber = '+1234567890'; await conversations.startConversation(userPhone, { name: 'John', step: 'greeting', }); // Send personalized message using template await conversations.sendMessage( userPhone, (context) => `Hello ${context.name}! Welcome to our service. Reply MENU for options.` ); } async function handleIncomingMessage( conversations: ConversationManager<UserContext>, message: SMSMessage ): Promise<void> { const phone = message.from as PhoneNumber; const context = conversations.getContext(phone); if (!context) { await conversations.sendMessage( phone, 'Welcome! Please start by saying HELLO.' ); return; } const input = message.body.toLowerCase().trim(); switch (context.step) { case 'greeting': if (input.includes('menu')) { conversations.updateContext(phone, { step: 'menu' }); await conversations.sendMessage( phone, 'Main Menu:\n1. Support\n2. Track Order\n3. Feedback\n\nReply with a number.' ); } break; case 'menu': switch (input) { case '1': conversations.updateContext(phone, { step: 'support' }); await conversations.sendMessage( phone, 'Support: Please describe your issue.' ); break; case '2': await conversations.sendMessage( phone, 'Please provide your order number:' ); break; case '3': conversations.updateContext(phone, { step: 'feedback' }); await conversations.sendMessage( phone, 'Feedback: Please share your experience with us.' ); break; } break; case 'support': await conversations.sendMessage( phone, `Thank you for contacting support. We've received your message: "${message.body}". A representative will respond soon.` ); conversations.updateContext(phone, { step: 'menu' }); break; case 'feedback': await conversations.sendMessage( phone, 'Thank you for your feedback! We appreciate your input.' ); conversations.updateContext(phone, { step: 'menu' }); break; } }
Running the Example
Step-by-Step Instructions
1
Start sms-dev
Run sms-dev
in your terminal
2
Compile TypeScript
Run npx tsc
or use ts-node
3
Run your application
Execute your compiled JavaScript or use ts-node app.ts