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