tzurot-shared-types
Type management for Tzurot v3 - Zod schemas, type guards, DTOs, workspace exports, and ensuring type safety across microservices. Use when creating types or working with data validation.
- Category: discovered
- Tags: github, community
- Version: 1.0.0
- Author: walid
- Downloads: 0
- GitHub stars: 6
- Updated: 2025-11-21
Download the latest version (v1.0.0) from this page.
SKILL.md
---
name: tzurot-shared-types
description: Type management for Tzurot v3 - Zod schemas, type guards, DTOs, workspace exports, and ensuring type safety across microservices. Use when creating types or working with data validation.
lastUpdated: '2025-11-19'
---
# Tzurot v3 Shared Types & Validation
**Use this skill when:** Creating new types, adding data validation, sharing types across services, or working with Discord/AI provider data structures.
## Core Principle
**In a microservices architecture, type mismatches between services cause silent failures.**
If bot-client pushes job data that ai-worker can't parse, the job fails silently with no user feedback. Type safety and runtime validation are CRITICAL.
## Type Organization
### Packages/Common-Types Structure
```
packages/common-types/src/
├── types/ # TypeScript interfaces
│ ├── discord-types.ts # Discord.js extensions
│ ├── ai-types.ts # AI provider request/response
│ ├── queue-types.ts # BullMQ job data types
│ ├── personality-types.ts
│ └── schemas.ts # Zod schemas for validation
│
├── services/ # Shared service classes
├── utils/ # Utility functions
├── constants/ # Application constants
└── index.ts # Barrel export
```
### When Types Belong in Common-Types
**✅ Add to common-types when:**
1. **Used by 2+ services** - Cross-service data contracts
2. **Queue job data** - BullMQ job payloads
3. **API request/response** - HTTP contract between bot-client and api-gateway
4. **Discord type extensions** - Reusable Discord.js type narrowing
5. **AI provider types** - OpenRouter/Gemini request/response shapes
**❌ Keep local when:**
1. **Service-specific** - Only used within one service
2. **Implementation details** - Internal data structures
3. **Temporary types** - Transitional refactoring types
## Zod Schemas for Runtime Validation
**Always use Zod for data entering service boundaries:**
### Why Zod?
- **Runtime validation** - TypeScript only checks at compile time
- **Type inference** - Generate TypeScript types from schemas
- **Error messages** - Clear validation failure details
- **Composable** - Build complex schemas from simple ones
### Schema Pattern
```typescript
import { z } from 'zod';
// Define Zod schema
export const PersonalityConfigSchema = z.object({
name: z.string().min(1).max(100),
systemPrompt: z.string().min(10),
model: z.string(),
temperature: z.number().min(0).max(2).optional(),
maxTokens: z.number().positive().optional(),
});
// Infer TypeScript type from schema
export type PersonalityConfig = z.infer<typeof PersonalityConfigSchema>;
// Validation function
export function validatePersonalityConfig(data: unknown): PersonalityConfig {
return PersonalityConfigSchema.parse(data);
}
```
### Usage in Services
```typescript
// api-gateway/routes/ai.ts
app.post('/ai/generate', (req, res) => {
try {
// Validate incoming request
const request = AIGenerationRequestSchema.parse(req.body);
// Type-safe from here on
const job = await queue.add('llm-generation', request);
res.json({ jobId: job.id });
} catch (error) {
if (error instanceof z.ZodError) {
return res.status(400).json({
error: 'Validation failed',
details: error.errors,
});
}
throw error;
}
});
```
## Type Guards
**Type guards narrow TypeScript types at runtime**
### Discord Type Guards
```typescript
// packages/common-types/src/types/discord-types.ts
import type { Message, TextChannel, DMChannel } from 'discord.js';
/**
* Type guard to check if a channel is a text-based channel
*/
export function isTextChannel(channel: any): channel is TextChannel {
return channel?.type === 0; // ChannelType.GuildText
}
/**
* Type guard to check if a channel is a DM
*/
export function isDMChannel(channel: any): channel is DMChannel {
return channel?.type === 1; // ChannelType.DM
}
/**
* Type guard for messages with attachments
*/
export function hasAttachments(message: Message): boolean {
return message.attachments.size > 0;
}
/**
* Type guard for voice message attachments
*/
export function hasVoiceMessage(message: Message): boolean {
return Array.from(message.attachments.values()).some(
attachment =>
attachment.contentType?.startsWith('audio/') ||
attachment.name?.endsWith('.ogg') ||
attachment.name?.endsWith('.mp3')
);
}
```
### Usage
```typescript
// bot-client/handlers/messageCreate.ts
import { isTextChannel, hasAttachments } from '@tzurot/common-types';
client.on('messageCreate', async message => {
// Type narrowing with guard
if (!isTextChannel(message.channel)) {
return; // Only handle text channels
}
// Now message.channel is typed as TextChannel
const permissions = message.channel.permissionsFor(client.user!);
if (hasAttachments(message)) {
// Handle attachments
}
});
```
## DTOs (Data Transfer Objects)
**DTOs define data shapes for cross-service communication**
### BullMQ Job Data
```typescript
// packages/common-types/src/types/queue-types.ts
import { z } from 'zod';
/**
* LLM Generation Job Data
* Sent from api-gateway to ai-worker via BullMQ
*/
export const LLMGenerationJobDataSchema = z.object({
requestId: z.string(),
personalityId: z.string(),
channelId: z.string(),
guildId: z.string().optional(),
userId: z.string(),
userMessage: z.string(),
conversationHistory: z.array(
z.object({
role: z.enum(['user', 'assistant', 'system']),
content: z.string(),
timestamp: z.string().datetime(),
})
),
attachments: z
.array(
z.object({
url: z.string().url(),
type: z.enum(['image', 'voice']),
description: z.string().optional(),
})
)
.optional(),
});
export type LLMGenerationJobData = z.infer<typeof LLMGenerationJobDataSchema>;
```
### HTTP API Request/Response
```typescript
// packages/common-types/src/types/ai-types.ts
import { z } from 'zod';
/**
* AI Generation Request
* POST /ai/generate from bot-client
*/
export const AIGenerationRequestSchema = z.object({
personalityName: z.string(),
message: z.string(),
channelId: z.string(),
guildId: z.string().optional(),
userId: z.string(),
username: z.string(),
referencedMessageId: z.string().optional(),
attachments: z
.array(
z.object({
url: z.string().url(),
contentType: z.string(),
name: z.string(),
})
)
.optional(),
});
export type AIGenerationRequest = z.infer<typeof AIGenerationRequestSchema>;
/**
* AI Generation Response
* Returned to bot-client
*/
export const AIGenerationResponseSchema = z.object({
content: z.string(),
model: z.string(),
personalityName: z.string(),
usage: z
.object({
promptTokens: z.number(),
completionTokens: z.number(),
totalTokens: z.number(),
})
.optional(),
});
export type AIGenerationResponse = z.infer<typeof AIGenerationResponseSchema>;
```
## Workspace Type Exports
### Package.json Exports
```json
// packages/common-types/package.json
{
"name": "@tzurot/common-types",
"version": "3.0.0-alpha.43",
"type": "module",
"exports": {
".": "./dist/index.js",
"./types": "./dist/types/index.js",
"./services": "./dist/services/index.js",
"./constants": "./dist/constants/index.js"
},
"typesVersions": {
"*": {
"*": ["./dist/*"]
}
}
}
```
### Barrel Exports
```typescript
// packages/common-types/src/index.ts
export * from './types/index.js';
export * from './services/index.js';
export * from './constants/index.js';
export * from './utils/index.js';
export * from './errors/index.js';
// packages/common-types/src/types/index.ts
export * from './discord-types.js';
export * from './ai-types.js';
export * from './queue-types.js';
export * from './personality-types.js';
export * from './schemas.js';
```
### Importing in Services
```typescript
// ✅ GOOD - Import from package
import {
AIGenerationRequest,
LLMGenerationJobData,
isTextChannel,
PersonalityConfigSchema,
} from '@tzurot/common-types';
// ❌ BAD - Direct file imports
import { AIGenerationRequest } from '@tzurot/common-types/dist/types/ai-types';
```
## Schema Composition
**Build complex schemas from simple ones**
```typescript
// Base schemas
const BaseMessageSchema = z.object({
role: z.enum(['user', 'assistant', 'system']),
content: z.string(),
timestamp: z.string().datetime(),
});
const AttachmentSchema = z.object({
url: z.string().url(),
type: z.enum(['image', 'voice', 'file']),
contentType: z.string(),
name: z.string(),
});
// Composed schemas
const UserMessageSchema = BaseMessageSchema.extend({
role: z.literal('user'),
userId: z.string(),
username: z.string(),
attachments: z.array(AttachmentSchema).optional(),
});
const AssistantMessageSchema = BaseMessageSchema.extend({
role: z.literal('assistant'),
model: z.string(),
usage: z
.object({
tokens: z.number(),
})
.optional(),
});
// Union type
const ConversationMessageSchema = z.union([UserMessageSchema, AssistantMessageSchema]);
export type ConversationMessage = z.infer<typeof ConversationMessageSchema>;
```
## Error Handling with Zod
```typescript
import { z } from 'zod';
function processJobData(data: unknown): void {
try {
const validated = LLMGenerationJobDataSchema.parse(data);
// Type-safe processing
console.log(`Processing request: ${validated.requestId}`);
} catch (error) {
if (error instanceof z.ZodError) {
// Detailed validation errors
console.error('Validation failed:');
error.errors.forEach(err => {
console.error(` ${err.path.join('.')}: ${err.message}`);
});
// Example error:
// conversationHistory.0.role: Invalid enum value. Expected 'user' | 'assistant' | 'system', received 'bot'
}
throw error;
}
}
```
## Type Safety Patterns
### Discriminated Unions
```typescript
// Job types with discriminator
type JobResult = { status: 'success'; data: string } | { status: 'error'; error: Error };
function handleJobResult(result: JobResult): void {
// TypeScript narrows type based on status
if (result.status === 'success') {
console.log(result.data); // ✅ data is available
} else {
console.error(result.error); // ✅ error is available
}
}
```
### Branded Types
```typescript
// Prevent mixing similar string types
type PersonalityId = string & { __brand: 'PersonalityId' };
type UserId = string & { __brand: 'UserId' };
function getPersonality(id: PersonalityId): Promise<Personality> {
// ...
}
const userId: UserId = 'user-123' as UserId;
const personalityId: PersonalityId = 'personality-456' as PersonalityId;
// ❌ TypeScript error - can't mix branded types
getPersonality(userId); // Error: UserId is not PersonalityId
```
### Const Assertions
```typescript
// Infer literal types
const ROLES = ['user', 'assistant', 'system'] as const;
type Role = (typeof ROLES)[number]; // 'user' | 'assistant' | 'system'
// Readonly object
const CONFIG = {
MAX_RETRIES: 3,
TIMEOUT: 5000,
} as const;
// CONFIG.MAX_RETRIES = 5; // ❌ Error: readonly
```
## Testing Types
### Type-Only Tests
```typescript
// Type assertions for compile-time checks
import { expectType, expectError } from 'tsd';
expectType<PersonalityConfig>({
name: 'Test',
systemPrompt: 'Test prompt',
model: 'test-model',
});
expectError<PersonalityConfig>({
name: 'Test',
// Missing required systemPrompt
});
```
### Runtime Validation Tests
```typescript
describe('AIGenerationRequestSchema', () => {
it('should validate correct request', () => {
const validRequest = {
personalityName: 'Lilith',
message: 'Hello',
channelId: '123',
userId: '456',
username: 'user',
};
const result = AIGenerationRequestSchema.parse(validRequest);
expect(result).toEqual(validRequest);
});
it('should reject invalid request', () => {
const invalidRequest = {
// Missing required fields
personalityName: 'Lilith',
};
expect(() => AIGenerationRequestSchema.parse(invalidRequest)).toThrow(z.ZodError);
});
});
```
## Anti-Patterns
### ❌ Don't Use `any`
```typescript
// ❌ BAD - Loses type safety
function processMessage(message: any): void {
console.log(message.content); // No type checking
}
// ✅ GOOD - Proper typing
function processMessage(message: Message): void {
console.log(message.content); // Type-safe
}
```
### ❌ Don't Skip Runtime Validation at Boundaries
```typescript
// ❌ BAD - Trusts external data
app.post('/ai/generate', (req, res) => {
const request = req.body as AIGenerationRequest; // Unsafe cast!
// What if req.body doesn't match the type?
});
// ✅ GOOD - Validate at boundary
app.post('/ai/generate', (req, res) => {
const request = AIGenerationRequestSchema.parse(req.body);
// Safe - validated at runtime
});
```
### ❌ Don't Duplicate Type Definitions
```typescript
// ❌ BAD - Duplication across services
// bot-client/types.ts
interface PersonalityConfig {
/* ... */
}
// ai-worker/types.ts
interface PersonalityConfig {
/* ... */
} // Same type!
// ✅ GOOD - Define once in common-types
// packages/common-types/src/types/personality-types.ts
export interface PersonalityConfig {
/* ... */
}
```
### ❌ Don't Use TypeScript Enums
```typescript
// ❌ BAD - TypeScript enum (generates runtime code)
enum MessageRole {
User = 'user',
Assistant = 'assistant',
System = 'system',
}
// ✅ GOOD - Zod enum or const assertion
const ROLES = ['user', 'assistant', 'system'] as const;
type MessageRole = (typeof ROLES)[number];
```
## Related Skills
- **tzurot-db-vector** - Prisma schema and database types
- **tzurot-async-flow** - BullMQ job data types
- **tzurot-testing** - Type-safe test fixtures
- **tzurot-security** - Input validation with Zod
## References
- Type centralization: `CLAUDE.md#type-centralization`
- Zod documentation: https://zod.dev/
- Common-types package: `packages/common-types/`
- TypeScript handbook: https://www.typescriptlang.org/docs/handbook/