Strict Typing Refactoring Examples
Note: This is a legacy document. For current patterns, see Zero Tolerance Rules, Logging Guide, and Error Handling Guide.
This document shows how to refactor actual code from the TendSocial backend to use explicit types.
Example 1: Performance Metrics (abTestResults.job.ts)
❌ BEFORE - Lines 51, 107, 120
typescript
const variants = test.variants as any[]; // LINE 51
// Later...
const metrics = post.performanceMetrics as any; // LINE 107
// Later...
results: results as any, // LINE 120✅ AFTER - Explicit Types
typescript
// At top of file
import { z } from 'zod';
import type { ABTestVariant } from '@/types/ab-testing.js';
// Define variant schema
const ABTestVariantSchema = z.object({
name: z.string(),
weight: z.number(),
provider: z.string(),
model: z.string(),
});
// LINE 51 - Validate variants
const variantsResult = z.array(ABTestVariantSchema).safeParse(test.variants);
if (!variantsResult.success) {
logger.error('Invalid A/B test variants', { testId: test.id });
continue;
}
const variants: ABTestVariant[] = variantsResult.data;
// LINE 107 - Use explicit PerformanceMetrics type
import { validateJsonField } from '@/types/index.js';
import { PerformanceMetricsSchema, type PerformanceMetrics } from '@/types/metrics.js';
const metrics: PerformanceMetrics | null = validateJsonField(
post.performanceMetrics,
PerformanceMetricsSchema
);
if (metrics !== null) {
results[variant].impressions += metrics.impressions ?? 0;
results[variant].engagements +=
(metrics.likes ?? 0) +
(metrics.comments ?? 0) +
(metrics.shares ?? 0) +
(metrics.clicks ?? 0);
}
// LINE 120 - Define explicit results type
interface ABTestVariantResults {
generations: number;
postsPublished: number;
impressions: number;
engagements: number;
costCents: number;
}
interface ABTestResults {
[variantName: string]: ABTestVariantResults;
}
const results: ABTestResults = {};
// Later when saving
await prisma.aIABTest.update({
where: { id: test.id },
data: {
results: results as object, // Prisma Json type accepts object
}
});Example 2: Example Curation (exampleCuration.job.ts)
❌ BEFORE - Lines 34, 62-63, 68-70, 86-87
typescript
} catch (error: any) { // LINE 34
console.error(error.message);
}
// Later...
.filter(p => (p.performanceMetrics as any)?.engagementRate > 0) // LINE 62
.sort((a, b) => ((b.performanceMetrics as any)?.engagementRate || 0) - ((a.performanceMetrics as any)?.engagementRate || 0)) // LINE 63
// Later...
platform: (p.generationContext as any)?.platform || 'unknown', // LINE 68
content: (p as any).content || p.originalContent || '', // LINE 69
engagementRate: (p.performanceMetrics as any)?.engagementRate, // LINE 70
// Later...
platform: (p.generationContext as any)?.platform || 'unknown', // LINE 86
content: (p as any).content || p.originalContent || '', // LINE 87✅ AFTER - Explicit Types
typescript
// At top of file
import type { ErrorResponse } from '@/types/errors.js';
import { isError, getErrorMessage } from '@/types/errors.js';
import { validateJsonField } from '@/types/index.js';
import {
PerformanceMetricsSchema,
type PerformanceMetrics
} from '@/types/metrics.js';
import {
type PostGenerationOptions,
type GenerationResult,
PostGenerationSchema
} from '@/types/index.js';
// LINE 34 - Explicit error type
} catch (error: Error | ErrorResponse) {
if (isError(error)) {
console.error(error.message);
logger.error(error.stack);
} else {
console.error(getErrorMessage(error));
}
}
// LINES 62-70 - Explicit types with validation
const postsWithMetrics: Array<{
post: typeof post;
metrics: PerformanceMetrics;
context: GenerationContext;
}> = bestPerformingPosts
.map(p => ({
post: p,
metrics: validateJsonField(p.performanceMetrics, PerformanceMetricsSchema),
context: validateJsonField(p.generationContext, GenerationContextSchema),
}))
.filter((item): item is { post: typeof post; metrics: PerformanceMetrics; context: GenerationContext } =>
item.metrics !== null &&
item.context !== null &&
item.metrics.engagementRate !== undefined &&
item.metrics.engagementRate > 0
);
const sortedByEngagement = postsWithMetrics
.sort((a, b) => (b.metrics.engagementRate ?? 0) - (a.metrics.engagementRate ?? 0))
.slice(0, 5);
interface BestPerformingExample {
postId: string;
platform: string;
content: string;
engagementRate: number;
reason: string;
}
const bestPerforming: BestPerformingExample[] = sortedByEngagement.map(item => ({
postId: item.post.id,
platform: item.context.platform ?? 'unknown',
content: item.post.content ?? item.post.originalContent ?? '',
engagementRate: item.metrics.engagementRate ?? 0,
reason: 'high_engagement'
}));Example 3: Platform Adapters (linkedin-pages.ts, etc.)
❌ BEFORE - Lines with 'any' in platform files
typescript
const metrics = post.performanceMetrics as any;
const account = account.metadata as any;✅ AFTER - Define Platform Metadata Types
typescript
// src/types/platforms.ts
export interface LinkedInPageMetadata {
organizationId: string;
organizationName: string;
organizationUrn?: string;
authorUrn?: string;
permissions?: string[];
}
export const LinkedInPageMetadataSchema = z.object({
organizationId: z.string(),
organizationName: z.string(),
organizationUrn: z.string().optional(),
authorUrn: z.string().optional(),
permissions: z.array(z.string()).optional(),
});
// In the adapter file
import { validateJsonField } from '@/types/index.js';
import {
LinkedInPostSchema,
type LinkedInPost,
type LinkedInResponse
} from '@/types/platforms.js';
const metadata: LinkedInPageMetadata | null = validateJsonField(
account.metadata,
LinkedInPageMetadataSchema
);
if (metadata === null) {
throw new Error('Invalid LinkedIn page metadata');
}
// Now use metadata.organizationId safely
const orgId: string = metadata.organizationId;Example 4: Error Handling in Jobs
❌ BEFORE - Multiple job files
typescript
} catch (error: any) {
console.error(`Failed:`, error.message);
throw error;
}✅ AFTER - Explicit Error Types
typescript
import type { ErrorResponse } from '@/types/errors.js';
import { isError, getErrorMessage } from '@/types/errors.js';
} catch (error: Error | ErrorResponse) {
const message: string = getErrorMessage(error);
logger.error('Job failed', {
jobType: 'performanceSync',
companyId,
error: message,
stack: isError(error) ? error.stack : undefined,
});
// Update job status
await prisma.job.update({
where: { id: jobId },
data: {
status: 'failed',
errorMessage: message,
}
});
throw error;
}Example 5: AI Gateway Adapters
❌ BEFORE - direct.adapter.ts
typescript
const data: any = await response.json();
return data as any;✅ AFTER - Define Response Types
typescript
// src/types/ai.ts
export interface OpenAICompletionResponse {
id: string;
object: string;
created: number;
model: string;
choices: Array<{
index: number;
message: {
role: string;
content: string;
};
finish_reason: string;
}>;
usage: {
prompt_tokens: number;
completion_tokens: number;
total_tokens: number;
};
}
export const OpenAICompletionResponseSchema = z.object({
id: z.string(),
object: z.string(),
created: z.number(),
model: z.string(),
choices: z.array(z.object({
index: z.number(),
message: z.object({
role: z.string(),
content: z.string(),
}),
finish_reason: z.string(),
})),
usage: z.object({
prompt_tokens: z.number(),
completion_tokens: z.number(),
total_tokens: z.number(),
}),
});
// In the adapter
import {
OpenAICompletionResponseSchema,
type OpenAICompletionResponse
} from '../types/ai/openai';
const rawData: string | number | boolean | object | null = await response.json();
const data: OpenAICompletionResponse = OpenAICompletionResponseSchema.parse(rawData);
return {
text: data.choices[0].message.content,
usage: {
inputTokens: data.usage.prompt_tokens,
outputTokens: data.usage.completion_tokens,
totalTokens: data.usage.total_tokens,
},
};Example 6: GenAI Client
❌ BEFORE - genaiClient.ts line 136
typescript
const posts = data.map((item: any) => {
// ... uses item.platform, item.content, etc
});✅ AFTER - Define AI Response Type
typescript
// src/types/ai.ts
export interface AIGeneratedPostItem {
platform: string;
profileType: 'personal' | 'business';
content: string;
imagePrompts?: string[];
}
export const AIGeneratedPostItemSchema = z.object({
platform: z.string(),
profileType: z.enum(['personal', 'business']),
content: z.string(),
imagePrompts: z.array(z.string()).optional(),
});
export const AIGeneratedPostsSchema = z.array(AIGeneratedPostItemSchema);
export type AIGeneratedPosts = z.infer<typeof AIGeneratedPostsSchema>;
// In genaiClient.ts
import {
AIGeneratedPostsSchema,
type AIGeneratedPosts,
type AIGeneratedPostItem
} from './types/ai/social-post-generation';
let text: string = result.text;
if (!text) throw new Error("No response from AI");
text = text.replace(/```json\n?/g, '').replace(/```/g, '').trim();
const rawData: string | number | boolean | object | null = JSON.parse(text);
const data: AIGeneratedPosts = AIGeneratedPostsSchema.parse(rawData);
const posts = data.map((item: AIGeneratedPostItem) => {
let platform: Platform;
const rawPlatform: string = (item.platform || '').toUpperCase();
// Map platform string to enum
if (rawPlatform.includes('LINKEDIN')) platform = Platform.LINKEDIN_PAGE;
// ... rest of mapping
return {
platform,
profileType: item.profileType,
content: item.content,
imagePrompts: item.imagePrompts ?? [],
// ... rest of fields
};
});Example 7: Route Handlers
❌ BEFORE - genai.ts
typescript
app.post('/api/generate', async (req, res) => {
const data = req.body; // implicit any
const result = await service.generate(data);
res.json(result);
});✅ AFTER - Explicit Request/Response Types
typescript
// src/types/api.ts
export interface GenerateSocialPostsRequest {
blogUrl: string;
brandingUrl?: string;
selectedProfiles: string[];
includeCta?: boolean;
hashtags?: string;
hashtagCount?: number;
imageCount?: number;
imageStyle?: string;
}
export const GenerateSocialPostsRequestSchema = z.object({
blogUrl: z.string().url(),
brandingUrl: z.string().url().optional(),
selectedProfiles: z.array(z.string()).min(1),
includeCta: z.boolean().optional(),
hashtags: z.string().optional(),
hashtagCount: z.number().int().positive().optional(),
imageCount: z.number().int().positive().optional(),
imageStyle: z.string().optional(),
});
export interface GenerateSocialPostsResponse {
posts: GeneratedPost[];
usage: number;
}
// In route file
import {
GenerateSocialPostsRequestSchema,
type GenerateSocialPostsRequest,
type GeminiResponse,
GeminiResponseSchema
} from '@/types/api.js';
import type { ErrorResponse } from '@/types/errors.js';
import { getErrorMessage } from '@/types/errors.js';
app.post('/api/generate', async (req, res) => {
try {
// Validate input
const input: GenerateSocialPostsRequest = GenerateSocialPostsRequestSchema.parse(req.body);
// Process
const result: GenerateSocialPostsResponse = await genaiService.generateSocialPosts(
req.companyId,
req.userId,
input.blogUrl,
input.brandingUrl ?? '',
input.selectedProfiles,
input.includeCta ?? true,
input.hashtags ?? '',
input.hashtagCount ?? 3,
input.imageCount ?? 1,
input.imageStyle ?? 'Photorealistic'
);
res.json(result);
} catch (error: Error | ErrorResponse | z.ZodError) {
if (error instanceof z.ZodError) {
res.status(400).json({
error: 'Validation failed',
details: error.format(),
});
} else {
res.status(500).json({
error: getErrorMessage(error),
});
}
}
});File Organization
Create this structure for all types:
src/types/
├── index.ts # Re-exports everything
├── errors.ts # Error types and guards
├── metrics.ts # PerformanceMetrics, EditMetrics
├── generation.ts # GenerationContext
├── ab-testing.ts # A/B test types
├── platforms/
│ ├── index.ts
│ ├── linkedin.ts # LinkedIn-specific types
│ ├── twitter.ts # Twitter-specific types
│ ├── facebook.ts
│ ├── instagram.ts
│ └── ...
├── ai/
│ ├── index.ts
│ ├── openai.ts # OpenAI response types
│ ├── anthropic.ts # Anthropic response types
│ ├── social-post-generation.ts
│ └── ...
└── api/
├── index.ts
├── genai.ts # GenAI API types
├── posts.ts # Posts API types
└── ...Quick Conversion Checklist
When you see any or unknown:
Identify what the data actually is
- External API response? → Create response type
- Prisma JSON field? → Create field type
- Function parameter? → Define parameter interface
- Error? → Use
Error | ErrorResponse
Create the type definition
typescriptexport interface MyType { field1: string; field2: number; }Create the Zod schema
typescriptexport const MyTypeSchema = z.object({ field1: z.string(), field2: z.number(), });Replace the
anywith the typetypescriptconst data: MyType = MyTypeSchema.parse(rawData);Add validation at the boundary
- API routes: validate req.body
- External APIs: validate response
- Database: validate JSON fields
Last Updated: January 2026