# Type Safety Best Practices Guide

## Overview

This guide provides comprehensive best practices for maintaining type safety across TypeScript and Python codebases in the BuzzTrail monorepo. Type safety is critical for our multi-platform deployment architecture and prevents runtime errors in production.

For detailed implementation instructions and CI/CD configuration, see the [Type Checking Guide](type-checking.md).

## Why Type Safety Matters at BuzzTrail

BuzzTrail deploys to four production environments simultaneously:
- **Railway**: API backend + Python bots (24/7 uptime requirements)
- **Vercel**: Serverless functions (auto-scaling, cold start sensitive)
- **GitHub Pages**: Static presentations (public-facing CDN)
- **Supabase**: Database/realtime (data integrity critical)

Type errors in any environment can cascade into complete system failures. Our comprehensive type checking system catches these errors before they reach production.

## TypeScript Best Practices

### 1. Use Explicit Types

Always provide explicit type annotations for function parameters and return types, even when TypeScript can infer them.

**Why**: Explicit types serve as documentation, catch errors at declaration site, and improve IDE autocomplete.

```typescript
// ❌ Bad - relies on inference
async function generateSlides(sessionId, templateId) {
  const result = await slideService.generate(sessionId, templateId);
  return result;
}

// ✅ Good - explicit types
async function generateSlides(
  sessionId: string,
  templateId: string
): Promise<GenerateResult> {
  const result = await slideService.generate(sessionId, templateId);
  return result;
}
```

### 2. Avoid Using `any`

The `any` type bypasses all type checking and should be avoided. Use specific types or `unknown` when you truly don't know the type.

**Why**: `any` defeats the purpose of TypeScript and hides potential bugs.

```typescript
// ❌ Bad - any disables type checking
function processData(data: any): any {
  return data.value.toUpperCase();
}

// ✅ Good - specific interface
interface ProcessData {
  value: string;
}

function processData(data: ProcessData): string {
  return data.value.toUpperCase();
}

// ✅ Acceptable - unknown for truly unknown data
function parseJSON(json: string): unknown {
  return JSON.parse(json);
}
```

**When `any` is acceptable**:
- Interacting with untyped third-party libraries (add `// @ts-expect-error` comment)
- Temporary scaffolding (must add TODO comment with issue number)
- Truly dynamic data that can't be typed (document why)

### 3. Use Type Guards

When working with union types or unknown data, use type guards to narrow types safely.

**Why**: Type guards provide runtime validation that matches compile-time types.

```typescript
// ❌ Bad - unsafe type assertion
function getLength(value: string | number): number {
  return (value as string).length;
}

// ✅ Good - type guard
function getLength(value: string | number): number {
  if (typeof value === 'string') {
    return value.length;
  }
  return value.toString().length;
}

// ✅ Good - custom type guard
interface User {
  id: string;
  name: string;
}

function isUser(value: unknown): value is User {
  return (
    typeof value === 'object' &&
    value !== null &&
    'id' in value &&
    'name' in value &&
    typeof (value as User).id === 'string' &&
    typeof (value as User).name === 'string'
  );
}

function processUser(data: unknown): User {
  if (!isUser(data)) {
    throw new Error('Invalid user data');
  }
  return data; // TypeScript knows this is User
}
```

### 4. Enable and Respect Strict Mode

All projects must use strict TypeScript configuration. Never disable strict checks to "fix" errors.

**Why**: Strict mode catches the most common sources of bugs: null/undefined handling, implicit any, and unchecked function types.

```json
// tsconfig.base.json
{
  "compilerOptions": {
    "strict": true,
    "noImplicitAny": true,
    "strictNullChecks": true,
    "strictFunctionTypes": true,
    "strictBindCallApply": true,
    "strictPropertyInitialization": true,
    "noImplicitThis": true,
    "alwaysStrict": true,
    "noUnusedLocals": true,
    "noUnusedParameters": true,
    "noImplicitReturns": true,
    "noFallthroughCasesInSwitch": true
  }
}
```

### 5. Handle Null and Undefined Explicitly

With `strictNullChecks` enabled, always check for null/undefined before accessing properties.

**Why**: Null/undefined errors are the most common runtime errors in JavaScript.

```typescript
// ❌ Bad - assumes user exists
async function getUserName(userId: string): Promise<string> {
  const user = await getUser(userId);
  return user.name; // Error: Object is possibly 'null'
}

// ✅ Good - explicit null check
async function getUserName(userId: string): Promise<string> {
  const user = await getUser(userId);
  if (!user) {
    throw new Error(`User ${userId} not found`);
  }
  return user.name;
}

// ✅ Good - optional chaining with default
async function getUserNameSafe(userId: string): Promise<string> {
  const user = await getUser(userId);
  return user?.name ?? 'Unknown';
}
```

### 6. Use Discriminated Unions for Result Types

Model success and error cases with discriminated unions instead of throwing exceptions.

**Why**: Makes error handling explicit and forces callers to handle all cases.

```typescript
// ❌ Bad - errors hidden in implementation
async function generateSlides(
  sessionId: string,
  templateId: string
): Promise<SlidesData> {
  // Throws on error - caller may not expect this
  const slides = await slideService.generate(sessionId, templateId);
  return slides;
}

// ✅ Good - discriminated union
type GenerateResult =
  | { success: true; data: SlidesData }
  | { success: false; error: string; code: ErrorCode };

async function generateSlides(
  sessionId: string,
  templateId: string
): Promise<GenerateResult> {
  try {
    const slides = await slideService.generate(sessionId, templateId);
    return { success: true, data: slides };
  } catch (error) {
    return {
      success: false,
      error: error instanceof Error ? error.message : 'Unknown error',
      code: 'GENERATION_FAILED',
    };
  }
}

// Usage - compiler enforces checking success
const result = await generateSlides(sessionId, templateId);
if (result.success) {
  console.log(result.data); // TypeScript knows data exists
} else {
  console.error(result.error); // TypeScript knows error exists
}
```

### 7. Leverage Generics for Reusable Code

Use generics to create reusable, type-safe utilities.

**Why**: Generics preserve type information through transformations.

```typescript
// ❌ Bad - loses type information
function getFirstItem(items: any[]): any {
  return items[0];
}

// ✅ Good - preserves type with generic
function getFirstItem<T>(items: T[]): T | undefined {
  return items[0];
}

// Usage - type is preserved
const numbers = [1, 2, 3];
const first = getFirstItem(numbers); // Type is number | undefined

// ✅ Good - generic with constraints
interface Identifiable {
  id: string;
}

function findById<T extends Identifiable>(items: T[], id: string): T | undefined {
  return items.find(item => item.id === id);
}
```

### 8. Use Readonly for Immutability

Mark properties as readonly when they shouldn't be modified after creation.

**Why**: Prevents accidental mutations and makes intent clear.

```typescript
// ❌ Bad - mutable config
interface Config {
  apiKey: string;
  endpoint: string;
}

// ✅ Good - immutable config
interface Config {
  readonly apiKey: string;
  readonly endpoint: string;
}

// ✅ Good - readonly arrays
function processItems(items: readonly string[]): number {
  // items.push('new'); // Error: push doesn't exist on readonly array
  return items.length;
}

// ✅ Good - Readonly utility type
type ImmutableUser = Readonly<User>;
```

### 9. Avoid Type Assertions (as)

Type assertions (`as`) bypass type checking and should be avoided. Use type guards instead.

**Why**: Assertions can lie and cause runtime errors.

```typescript
// ❌ Bad - unsafe assertion
function processElement(element: Element): void {
  const div = element as HTMLDivElement;
  div.classList.add('active'); // Crashes if element is not a div
}

// ✅ Good - type guard with runtime check
function processElement(element: Element): void {
  if (!(element instanceof HTMLDivElement)) {
    throw new Error('Element must be a div');
  }
  element.classList.add('active'); // TypeScript knows it's a div
}

// ✅ Acceptable - assertion after validation
function processElement(element: Element): void {
  if (element.tagName !== 'DIV') {
    throw new Error('Element must be a div');
  }
  // Safe because we checked
  const div = element as HTMLDivElement;
  div.classList.add('active');
}
```

### 10. Use Const Assertions for Literal Types

Use `as const` to create narrow literal types instead of wider primitive types.

**Why**: Literal types are more precise and catch errors earlier.

```typescript
// ❌ Bad - type is string
const status = 'active'; // Type: string

// ✅ Good - type is literal 'active'
const status = 'active' as const; // Type: 'active'

// ✅ Good - readonly tuple
const coordinates = [10, 20] as const; // Type: readonly [10, 20]

// ✅ Good - readonly object with literal types
const config = {
  mode: 'production',
  version: 1,
} as const;
// Type: { readonly mode: 'production'; readonly version: 1; }
```

## Python Best Practices

### 1. Use Type Hints for All Functions

All function parameters and return types must have type annotations.

**Why**: Type hints enable mypy validation and serve as documentation.

```python
# ❌ Bad - no type hints
def process_message(data):
    return data['content'].upper()

# ✅ Good - complete type hints
from typing import Dict, Any

def process_message(data: Dict[str, Any]) -> str:
    """
    Process message data and return uppercase content.

    Args:
        data: Dictionary containing message content

    Returns:
        Uppercase message content
    """
    return data['content'].upper()
```

### 2. Use TypedDict for Structured Dictionaries

Use TypedDict to define the structure of dictionary data instead of plain `Dict[str, Any]`.

**Why**: TypedDict provides structure validation and enables IDE autocomplete.

```python
# ❌ Bad - untyped dictionary
from typing import Dict, Any

def create_user(data: Dict[str, Any]) -> Dict[str, Any]:
    return {
        'id': data['user_id'],
        'name': data['full_name'],
        'email': data['email_address'],
    }

# ✅ Good - TypedDict for structure
from typing import TypedDict

class UserInput(TypedDict):
    user_id: str
    full_name: str
    email_address: str

class UserOutput(TypedDict):
    id: str
    name: str
    email: str

def create_user(data: UserInput) -> UserOutput:
    return {
        'id': data['user_id'],
        'name': data['full_name'],
        'email': data['email_address'],
    }
```

### 3. Use Literal Types for String Constants

Use `Literal` to restrict values to specific string constants.

**Why**: Catches typos and invalid values at type-check time.

```python
# ❌ Bad - any string accepted
def set_status(status: str) -> None:
    # Typo like 'actve' won't be caught
    database.update_status(status)

# ✅ Good - only valid values accepted
from typing import Literal

Status = Literal['active', 'inactive', 'pending']

def set_status(status: Status) -> None:
    # mypy catches typos like 'actve'
    database.update_status(status)
```

### 4. Use Optional for Nullable Values

Use `Optional[T]` for values that can be None.

**Why**: Makes nullability explicit in the type signature.

```python
# ❌ Bad - nullability unclear
from typing import Dict

def find_user(user_id: str) -> Dict:
    # Returns None on not found, but type doesn't indicate this
    return users.get(user_id)

# ✅ Good - Optional indicates None is possible
from typing import Optional, TypedDict

class User(TypedDict):
    id: str
    name: str

def find_user(user_id: str) -> Optional[User]:
    return users.get(user_id)

# Usage - forces null check
user = find_user('123')
if user is None:
    raise ValueError('User not found')
print(user['name'])  # Safe - mypy knows user is not None
```

### 5. Type Async Functions Correctly

Async functions must return coroutines, not the actual value type.

**Why**: Ensures proper async/await usage and type checking.

```python
# ❌ Bad - missing Coroutine type
from typing import Dict

async def fetch_data() -> Dict:
    return await client.get('/data')

# ✅ Good - explicit return type for async
from typing import Dict, Any

async def fetch_data() -> Dict[str, Any]:
    """
    Fetch data from API.

    Returns:
        Dictionary containing API response data
    """
    return await client.get('/data')

# ✅ Good - with proper error handling
from typing import Optional

async def fetch_data_safe() -> Optional[Dict[str, Any]]:
    try:
        return await client.get('/data')
    except Exception as error:
        logger.error(f"Failed to fetch data: {error}")
        return None
```

### 6. Avoid Bare `Dict` and `List` Types

Always specify the key/value types for Dict and element types for List.

**Why**: More specific types catch more errors and improve documentation.

```python
# ❌ Bad - unspecified types
from typing import Dict, List

def process_users(users: List) -> Dict:
    return {user['id']: user for user in users}

# ✅ Good - specific types
from typing import Dict, List, TypedDict

class User(TypedDict):
    id: str
    name: str
    email: str

def process_users(users: List[User]) -> Dict[str, User]:
    return {user['id']: user for user in users}
```

### 7. Use Protocols for Interface-Like Types

Use Protocol for structural typing (duck typing with types).

**Why**: Defines contracts without requiring inheritance.

```python
# ❌ Bad - no interface definition
def save_data(storage, data):
    storage.save(data)

# ✅ Good - Protocol defines interface
from typing import Protocol, Any

class DataStorage(Protocol):
    def save(self, data: Any) -> None:
        ...

    def load(self) -> Any:
        ...

def save_data(storage: DataStorage, data: Any) -> None:
    storage.save(data)

# Any class with save() and load() methods works
class FileStorage:
    def save(self, data: Any) -> None:
        # Implementation
        pass

    def load(self) -> Any:
        # Implementation
        pass

# mypy accepts this because it matches the Protocol
file_storage = FileStorage()
save_data(file_storage, {'key': 'value'})
```

### 8. Handle Optional TypedDict Fields Correctly

Use `total=False` for TypedDict with optional fields.

**Why**: Matches JavaScript's optional properties pattern.

```python
# ❌ Bad - all fields required
from typing import TypedDict

class Config(TypedDict):
    api_key: str
    endpoint: str
    timeout: int  # Should be optional but isn't

# ✅ Good - total=False for optional fields
class Config(TypedDict, total=False):
    api_key: str  # Required (document in docstring)
    endpoint: str  # Required
    timeout: int  # Optional

# ✅ Better - explicit with NotRequired (Python 3.11+)
from typing import Required, NotRequired

class Config(TypedDict):
    api_key: Required[str]
    endpoint: Required[str]
    timeout: NotRequired[int]
```

### 9. Use Type Aliases for Complex Types

Create type aliases for complex or frequently used type expressions.

**Why**: Improves readability and maintains consistency.

```python
# ❌ Bad - repeated complex type
from typing import Dict, List, Union

def process_a(data: Union[str, int, Dict[str, List[str]]]) -> None:
    pass

def process_b(data: Union[str, int, Dict[str, List[str]]]) -> None:
    pass

# ✅ Good - type alias
from typing import Dict, List, Union

ProcessData = Union[str, int, Dict[str, List[str]]]

def process_a(data: ProcessData) -> None:
    pass

def process_b(data: ProcessData) -> None:
    pass
```

### 10. Add Docstrings with Type Information

Docstrings should complement type hints with additional context.

**Why**: Type hints show what, docstrings explain why and how.

```python
# ❌ Bad - no documentation
from typing import Optional

def retry_operation(func, max_attempts: int) -> Optional[bool]:
    for attempt in range(max_attempts):
        if func():
            return True
    return None

# ✅ Good - comprehensive docstring
from typing import Callable, Optional

def retry_operation(
    func: Callable[[], bool],
    max_attempts: int
) -> Optional[bool]:
    """
    Retry an operation multiple times until success or max attempts reached.

    This function is useful for handling transient failures in network
    operations or other unreliable external dependencies.

    Args:
        func: A callable that returns True on success, False on failure
        max_attempts: Maximum number of retry attempts (must be positive)

    Returns:
        True if operation succeeded within max_attempts,
        None if all attempts failed

    Raises:
        ValueError: If max_attempts is less than 1

    Example:
        >>> def flaky_network_call() -> bool:
        ...     return random.random() > 0.5
        >>> result = retry_operation(flaky_network_call, 3)
    """
    if max_attempts < 1:
        raise ValueError("max_attempts must be at least 1")

    for attempt in range(max_attempts):
        if func():
            return True
    return None
```

## Cross-Language Type Synchronization

BuzzTrail uses shared types between TypeScript and Python. Follow these practices to maintain synchronization.

### 1. TypeScript as Source of Truth

All shared types are defined in TypeScript first under `libs/shared/types/src/lib/` and then manually translated to Python TypedDict.

**Why**: TypeScript has better tooling for generating and validating types.

**Workflow**:
1. Define or modify types in TypeScript
2. Update corresponding Python TypedDict in `libs/shared/types/shared_types.py`
3. Run both type checkers to validate
4. Commit TypeScript and Python changes together

### 2. Maintain Field Name Consistency

Field names must match exactly between TypeScript and Python (including case).

```typescript
// TypeScript (canonical)
export interface ConversationSession {
  id: string;
  sessionType: string;
  createdAt: string;
}
```

```python
# Python (mirror)
from typing import TypedDict

class ConversationSession(TypedDict):
    id: str
    sessionType: str  # Must match TypeScript case
    createdAt: str
```

### 3. Use Type Mapping Reference

Follow the type mapping table in `libs/shared/types/TYPES.md` for accurate translations:

| TypeScript | Python | Notes |
|------------|--------|-------|
| `string` | `str` | Direct mapping |
| `number` | `int` or `float` | Use `int` for IDs/counts |
| `boolean` | `bool` | Direct mapping |
| `T[]` | `List[T]` | Requires `from typing import List` |
| `T \| null` | `Optional[T]` | Use `total=False` for optional fields |
| `'a' \| 'b'` | `Literal['a', 'b']` | Requires `from typing import Literal` |

### 4. Version and Document Changes

Update version numbers and sync dates in `shared_types.py` when making changes:

```python
"""
Shared type definitions synchronized from TypeScript.

Version: 1.1.0
Last synced: 2025-10-14
Canonical source: libs/shared/types/src/lib/
"""
```

See [libs/shared/types/TYPES.md](../../libs/shared/types/TYPES.md) for the complete synchronization guide.

## IDE Configuration

### TypeScript (VS Code)

Recommended settings for `.vscode/settings.json`:

```json
{
  "typescript.tsdk": "node_modules/typescript/lib",
  "typescript.enablePromptUseWorkspaceTsdk": true,
  "typescript.preferences.importModuleSpecifier": "relative",
  "typescript.suggest.autoImports": true,
  "typescript.updateImportsOnFileMove.enabled": "always",
  "editor.codeActionsOnSave": {
    "source.organizeImports": true,
    "source.fixAll.eslint": true
  },
  "eslint.validate": [
    "javascript",
    "javascriptreact",
    "typescript",
    "typescriptreact"
  ]
}
```

### Python (VS Code)

Recommended settings:

```json
{
  "python.analysis.typeCheckingMode": "strict",
  "python.linting.mypyEnabled": true,
  "python.linting.enabled": true,
  "editor.codeActionsOnSave": {
    "source.organizeImports": true
  }
}
```

## Common Anti-Patterns to Avoid

### 1. Type Casting Instead of Fixing

```typescript
// ❌ Bad - masking the real issue
const result = someFunction() as ExpectedType;

// ✅ Good - fix the function's return type
function someFunction(): ExpectedType {
  // Implementation
}
```

### 2. Excessive Optional Properties

```typescript
// ❌ Bad - too many optional properties
interface Config {
  apiKey?: string;
  endpoint?: string;
  timeout?: number;
  retries?: number;
  // Everything optional makes type checking useless
}

// ✅ Good - required properties with sensible defaults
interface Config {
  apiKey: string;  // Required
  endpoint: string;  // Required
  timeout?: number;  // Truly optional
  retries?: number;  // Truly optional
}

const DEFAULT_CONFIG: Required<Omit<Config, 'apiKey' | 'endpoint'>> = {
  timeout: 5000,
  retries: 3,
};
```

### 3. Using `@ts-ignore` Without Comments

```typescript
// ❌ Bad - silent type suppression
// @ts-ignore
const value = unreliableFunction();

// ✅ Good - documented suppression with context
// @ts-ignore: Third-party library has incorrect types
// TODO(#123): Update types when library is fixed
const value = unreliableFunction();
```

### 4. Not Narrowing Union Types

```typescript
// ❌ Bad - doesn't narrow type
function process(value: string | number) {
  return value.toFixed(2); // Error: toFixed doesn't exist on string
}

// ✅ Good - narrows type properly
function process(value: string | number): string {
  if (typeof value === 'number') {
    return value.toFixed(2);
  }
  return value;
}
```

## Type Safety Checklist

Before committing code, verify:

### TypeScript
- [ ] All function parameters have explicit types
- [ ] All function return types are explicit
- [ ] No `any` types (or documented with TODO)
- [ ] All null/undefined cases handled
- [ ] Union types properly narrowed with type guards
- [ ] No type assertions without runtime validation
- [ ] Generics used where appropriate
- [ ] `pnpm nx typecheck <project>` passes
- [ ] `pnpm nx lint <project>` passes

### Python
- [ ] All function parameters have type hints
- [ ] All function return types specified
- [ ] TypedDict used for structured dictionaries
- [ ] Literal types used for string constants
- [ ] Optional used for nullable values
- [ ] Async functions properly typed
- [ ] Docstrings complement type hints
- [ ] `mypy <file>.py --strict` passes

### Cross-Language
- [ ] TypeScript and Python types synchronized
- [ ] Field names match exactly (case-sensitive)
- [ ] Version numbers updated in shared_types.py
- [ ] Both type checkers pass after changes

## Additional Resources

### Internal Documentation
- [Type Checking Guide](type-checking.md) - Complete CI/CD and local development setup
- [libs/shared/types/TYPES.md](../../libs/shared/types/TYPES.md) - Type synchronization guide
- [libs/shared/types/README.md](../../libs/shared/types/README.md) - Usage examples
- [CONTRIBUTING.md](../../CONTRIBUTING.md) - CI/CD requirements and workflow

### External Resources
- [TypeScript Handbook](https://www.typescriptlang.org/docs/handbook/intro.html)
- [TypeScript Deep Dive](https://basarat.gitbook.io/typescript/)
- [mypy Documentation](https://mypy.readthedocs.io/)
- [PEP 484 - Type Hints](https://peps.python.org/pep-0484/)
- [typing Module Documentation](https://docs.python.org/3/library/typing.html)

### Style Guides
- [Google TypeScript Style Guide](https://google.github.io/styleguide/tsguide.html)
- [Airbnb JavaScript Style Guide](https://github.com/airbnb/javascript)
- [Google Python Style Guide](https://google.github.io/styleguide/pyguide.html)
- [PEP 8 - Python Style Guide](https://peps.python.org/pep-0008/)

## Getting Help

1. Check this guide for best practices
2. See [Type Checking Guide](type-checking.md) for implementation details
3. Review PR comments - CI bot includes type error summaries
4. Ask in #engineering Slack channel
5. Pair program on complex type issues

---

**Last Updated**: 2025-10-14
**Maintained By**: Engineering Team
**Questions?**: Post in #engineering or open an issue
