Skip to content

Exposing Types for TypeScript Generation

The @ExposeType decorator allows you to explicitly expose types for TypeScript interface generation without requiring them to be directly referenced in REST endpoints, WebSocket messages, or as indirect dependencies.

Use Cases

The @ExposeType decorator is useful when you have types that:

  1. Are used only on the frontend - Types that exist for frontend state management, validation, or business logic
  2. Are part of a shared schema - Common types that multiple parts of your application use but aren't directly in API contracts
  3. Need to be pre-generated - Types that you want available immediately even if they're not yet used in any endpoints
  4. Are utility types - Helper types, enums, or constants that the frontend needs to know about

Basic Usage

Simply decorate any Pydantic model with @ExposeType():

from pydantic import BaseModel

from jararaca import ExposeType


@ExposeType()
class UserPermission(BaseModel):
    id: str
    name: str
    description: str
    resource: str
    action: str

This type will now be included in the generated TypeScript output when you run:

jararaca gen-tsi app:app output.ts

Example: Frontend-Only Types

from pydantic import BaseModel

from jararaca import ExposeType


@ExposeType()
class FilterState(BaseModel):
    """Frontend state for table filtering."""
    search_query: str
    sort_column: str
    sort_direction: str
    page: int
    page_size: int

@ExposeType()
class UITheme(BaseModel):
    """Frontend theme configuration."""
    primary_color: str
    secondary_color: str
    dark_mode: bool

Example: Error Codes and Constants

from enum import Enum

from pydantic import BaseModel

from jararaca import ExposeType


@ExposeType()
class ErrorCode(str, Enum):
    """Standard error codes."""
    UNAUTHORIZED = "UNAUTHORIZED"
    NOT_FOUND = "NOT_FOUND"
    VALIDATION_ERROR = "VALIDATION_ERROR"
    INTERNAL_ERROR = "INTERNAL_ERROR"

@ExposeType()
class ApiErrorDetail(BaseModel):
    """Detailed error information."""
    code: ErrorCode
    message: str
    field: str | None = None
    details: dict[str, str] | None = None

Example: Complex Nested Types

from pydantic import BaseModel

from jararaca import ExposeType


@ExposeType()
class Address(BaseModel):
    street: str
    city: str
    country: str
    postal_code: str

@ExposeType()
class ContactInfo(BaseModel):
    email: str
    phone: str | None = None
    address: Address

@ExposeType()
class Organization(BaseModel):
    """Complete organization structure."""
    id: str
    name: str
    contacts: list[ContactInfo]
    settings: dict[str, str]

When you expose a type with nested structures, the decorator ensures that all related types are also included in the TypeScript generation.

Comparison: With vs Without @ExposeType

Without @ExposeType

class UserRole(BaseModel):
    """Only generated if used in an endpoint or as a dependency."""
    id: str
    name: str

@RestController("/api/users")
class UserController:
    @Get("/{user_id}")
    async def get_user(self, user_id: str) -> UserResponse:
        # UserRole is only generated if UserResponse references it
        return UserResponse(...)

With @ExposeType

@ExposeType()
class UserRole(BaseModel):
    """Always generated, available immediately."""
    id: str
    name: str

@RestController("/api/users")
class UserController:
    @Get("/{user_id}")
    async def get_user(self, user_id: str) -> UserResponse:
        # UserRole is available in TypeScript even if not used yet
        return UserResponse(...)

Integration with Other Decorators

The @ExposeType decorator works seamlessly with other TypeScript generation decorators:

from jararaca import ExposeType, SplitInputOutput


@ExposeType()
@SplitInputOutput()
class UserProfile(BaseModel):
    """Generates UserProfileInput and UserProfileOutput interfaces."""
    id: str
    username: str
    email: str
    created_at: str
    updated_at: str

This creates both UserProfileInput and UserProfileOutput TypeScript interfaces.

Best Practices

  1. Use for shared types: Apply @ExposeType to types that are used across multiple parts of your application
  2. Document the purpose: Add clear docstrings explaining why a type is exposed
  3. Avoid overuse: Only expose types that the frontend actually needs - don't expose internal implementation details
  4. Combine with other decorators: Use alongside @SplitInputOutput when appropriate
  5. Group related types: Keep exposed types in dedicated modules (e.g., shared_types.py)

Viewing Exposed Types

All types decorated with @ExposeType are tracked globally. You can check which types are exposed:

from jararaca.tools.typescript.decorators import ExposeType

# Get all exposed types
exposed = ExposeType.get_all_exposed_types()
print(f"Exposed {len(exposed)} types: {[t.__name__ for t in exposed]}")

Generated TypeScript

Given this Python code:

@ExposeType()
class NotificationPreference(BaseModel):
    email_enabled: bool
    push_enabled: bool
    frequency: str

The generated TypeScript will be:

export interface NotificationPreference {
  emailEnabled: boolean;
  pushEnabled: boolean;
  frequency: string;
}

The type is available in your TypeScript code even if no REST endpoint uses it yet.