Examples/TypeScript

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