Custom Message Types
Extend melony with your own message structures and types using TypeScript generics.
Overview
melony supports custom message structures through TypeScript generics and mappers. This allows you to define your own part types and message formats while maintaining full type safety.
Defining Custom Types
Start by defining your custom part type that extends the base melony structure:
// Define your custom part type
type CustomPart = {
melonyId: string;
type: "text" | "image" | "tool_call";
role: "user" | "assistant" | "system";
text?: string;
imageUrl?: string;
toolName?: string;
toolArgs?: Record<string, any>;
};
Required Fields
melonyId
- Unique identifier for the parttype
- The type of content (text, image, tool_call, etc.)role
- Who sent the message (user, assistant, system)Using Custom Types
Use your custom types with the MelonyProvider and hooks:
"use client";
import {
MelonyProvider,
useMelonyMessages,
useMelonySend,
useMelonyStatus,
} from "melony";
// Define your custom part type
type CustomPart = {
melonyId: string;
type: "text" | "image" | "tool_call";
role: "user" | "assistant" | "system";
text?: string;
imageUrl?: string;
toolName?: string;
toolArgs?: Record<string, any>;
};
function ChatWithCustomTypes() {
const messages = useMelonyMessages<CustomPart>();
const send = useMelonySend();
const status = useMelonyStatus();
return (
<div>
{messages.map((message) => (
<div key={message.id}>
<strong>{message.role}:</strong>
{message.parts.map((part, i) => (
<div key={i}>
{part.type === "text" && <p>{part.text}</p>}
{part.type === "image" && (
<img src={part.imageUrl} alt="User uploaded image" />
)}
{part.type === "tool_call" && (
<div>
<strong>Tool:</strong> {part.toolName}
<pre>{JSON.stringify(part.toolArgs, null, 2)}</pre>
</div>
)}
</div>
))}
</div>
))}
<button onClick={() => send("Hello!")} disabled={status === "streaming"}>
{status === "streaming" ? "Sending..." : "Send"}
</button>
</div>
);
}
export default function Chat() {
return (
<MelonyProvider<CustomPart> endpoint="/api/chat">
<ChatWithCustomTypes />
</MelonyProvider>
);
}
Advanced Custom Types
You can create more complex custom types with additional metadata and validation:
// Advanced custom part with metadata
type AdvancedPart = {
melonyId: string;
type: "text" | "image" | "video" | "audio" | "file" | "code_block" | "tool_call";
role: "user" | "assistant" | "system" | "function";
// Text content
text?: string;
language?: string; // For code blocks
// Media content
imageUrl?: string;
videoUrl?: string;
audioUrl?: string;
fileUrl?: string;
fileName?: string;
fileSize?: number;
// Tool calls
toolName?: string;
toolArgs?: Record<string, any>;
toolResult?: any;
// Metadata
timestamp?: number;
metadata?: Record<string, any>;
// UI hints
isStreaming?: boolean;
isError?: boolean;
errorMessage?: string;
};
Type Mappers
You can create custom mappers to transform server responses into your custom types:
// Custom mapper function
function mapServerResponseToCustomPart(serverData: any): CustomPart {
return {
melonyId: serverData.id || crypto.randomUUID(),
type: serverData.type,
role: serverData.role,
text: serverData.content,
imageUrl: serverData.image_url,
toolName: serverData.function_name,
toolArgs: serverData.function_arguments,
};
}
// Use with custom mapper
const messages = useMelonyMessages<CustomPart>({
mapper: mapServerResponseToCustomPart,
});
Type Validation
Add runtime validation to ensure your custom types are properly structured:
// Validation function
function validateCustomPart(part: any): part is CustomPart {
return (
typeof part === 'object' &&
typeof part.melonyId === 'string' &&
typeof part.type === 'string' &&
typeof part.role === 'string' &&
['text', 'image', 'tool_call'].includes(part.type) &&
['user', 'assistant', 'system'].includes(part.role)
);
}
// Use with validation
const messages = useMelonyMessages<CustomPart>({
validator: validateCustomPart,
});
Real-World Example
Here's a complete example of a chat interface with custom message types:
"use client";
import { MelonyProvider, useMelonyMessages, useMelonySend } from "melony";
// Define custom part types for a rich chat interface
type RichPart = {
melonyId: string;
type: "text" | "image" | "code" | "tool_call" | "error";
role: "user" | "assistant" | "system";
text?: string;
imageUrl?: string;
code?: string;
language?: string;
toolName?: string;
toolArgs?: Record<string, any>;
errorMessage?: string;
};
function RichChatInterface() {
const messages = useMelonyMessages<RichPart>();
const send = useMelonySend();
const renderPart = (part: RichPart) => {
switch (part.type) {
case "text":
return <p className="text-content">{part.text}</p>;
case "image":
return (
<div className="image-container">
<img src={part.imageUrl} alt="Chat image" className="max-w-full rounded" />
</div>
);
case "code":
return (
<pre className="code-block">
<code className={`language-${part.language}`}>
{part.code}
</code>
</pre>
);
case "tool_call":
return (
<div className="tool-call">
<strong>🔧 {part.toolName}</strong>
<pre>{JSON.stringify(part.toolArgs, null, 2)}</pre>
</div>
);
case "error":
return (
<div className="error-message text-red-500">
❌ {part.errorMessage}
</div>
);
default:
return null;
}
};
return (
<div className="chat-container">
{messages.map((message) => (
<div key={message.id} className={`message ${message.role}`}>
<div className="message-header">
<strong>{message.role}</strong>
</div>
<div className="message-content">
{message.parts.map((part, i) => (
<div key={i} className="part">
{renderPart(part)}
</div>
))}
</div>
</div>
))}
</div>
);
}
export default function RichChat() {
return (
<MelonyProvider<RichPart> endpoint="/api/chat">
<RichChatInterface />
</MelonyProvider>
);
}
Best Practices
Keep types focused
Define types that match your specific use case rather than trying to support every possible message type.
Use TypeScript strictly
Enable strict TypeScript settings to catch type errors at compile time.
Add runtime validation
Include validation functions to catch runtime errors and provide better error messages.
Document your types
Add JSDoc comments to explain the purpose and structure of your custom types.