Complete guide to defining domain models using TypeSpec with code generation.
“Domain models are the single source of truth. TypeScript types are generated artifacts.”
TypeSpec-First Architecture ensures:
┌────────────────────────────────────────────────────────────────┐
│ │
│ 1. Define TypeSpec models (tsp/*.tsp) │
│ │
│ 2. Compile → Generate TypeScript + OpenAPI + JSON Schema │
│ pnpm tsp:compile │
│ │
│ 3. Import generated types in application code │
│ import type { Settings } from '@/domain/generated/output' │
│ │
│ 4. Build TypeScript → Compile to JavaScript │
│ pnpm build │
│ │
│ 5. Run tests → Verify types and behavior │
│ pnpm test │
│ │
└────────────────────────────────────────────────────────────────┘
tsp/
├── main.tsp # Entry point (imports all models)
├── common/ # Shared types
│ ├── base.tsp # BaseEntity, SoftDeletableEntity, AuditableEntity
│ ├── scalars.tsp # UUID scalar
│ ├── ask.tsp # Askable interface pattern
│ └── enums/ # Shared enumerations
│ ├── lifecycle.tsp # SdlcLifecycle enum
│ ├── status.tsp # TaskStatus enum
│ └── ...
├── domain/ # Domain layer models
│ ├── entities/ # One file per entity
│ │ ├── feature.tsp # Feature entity
│ │ ├── task.tsp # Task entity
│ │ ├── settings.tsp # Settings entity
│ │ └── ...
│ └── value-objects/ # Embedded value objects
│ ├── gantt.tsp # GanttChart value object
│ └── ...
├── agents/ # Agent system models
│ ├── analyze.tsp # Analyze agent operations
│ ├── requirements.tsp # Requirements agent operations
│ └── ...
└── deployment/ # Deployment configuration
├── target.tsp # DeployTarget model
├── skill.tsp # DeploySkill model
└── ...
# After running: pnpm tsp:compile
apis/
├── openapi/
│ └── openapi.yaml # OpenAPI 3.x spec (API documentation)
└── json-schema/ # JSON Schema files (one per model)
├── Feature.json
├── Task.json
├── Settings.json
└── ...
src/domain/generated/
└── output.ts # TypeScript types (DO NOT EDIT)
// tsp/domain/entities/settings.tsp
import "../common/base.tsp";
import "../common/enums/log-level.tsp";
/**
* Global application settings (singleton).
* Stored at ~/.shep/data as single SQLite record.
*/
model Settings extends BaseEntity {
/** Singleton ID (always 'singleton') */
id: "singleton";
/** Model configuration for different agents */
models: ModelConfiguration;
/** User profile information (optional) */
user: UserProfile;
/** Environment configuration */
environment: EnvironmentConfig;
/** System configuration */
system: SystemConfig;
}
/**
* AI model configuration for different agents.
*/
model ModelConfiguration {
/** Model for analyze agent (e.g., 'claude-opus-4') */
analyze: string;
/** Model for requirements agent */
requirements: string;
/** Model for plan agent */
plan: string;
/** Model for implementation agent */
implement: string;
}
/**
* User profile information.
*/
model UserProfile {
/** User's full name */
name?: string;
/** User's email address */
email?: string;
/** GitHub username */
githubUsername?: string;
}
/**
* Environment configuration.
*/
model EnvironmentConfig {
/** Default text editor (vim, nano, code, etc.) */
defaultEditor: string;
/** Preferred shell (bash, zsh, fish, etc.) */
shellPreference: string;
}
/**
* System configuration.
*/
model SystemConfig {
/** Enable automatic updates */
autoUpdate: boolean;
/** Logging level */
logLevel: LogLevel;
}
// tsp/common/enums/log-level.tsp
/**
* Logging level for system output.
*/
enum LogLevel {
/** Debug level logging (most verbose) */
debug,
/** Informational messages */
info,
/** Warning messages */
warn,
/** Error messages only */
error,
}
// tsp/common/base.tsp
/**
* Base entity with ID and timestamps.
* All entities should extend this model.
*/
model BaseEntity {
/** Unique identifier */
id: string;
/** Creation timestamp */
@encode(DateTimeKnownEncoding.rfc3339)
createdAt: utcDateTime;
/** Last update timestamp */
@encode(DateTimeKnownEncoding.rfc3339)
updatedAt: utcDateTime;
}
/**
* Entity with soft delete support.
*/
model SoftDeletableEntity extends BaseEntity {
/** Soft delete flag */
isDeleted: boolean = false;
/** Deletion timestamp (null if not deleted) */
@encode(DateTimeKnownEncoding.rfc3339)
deletedAt?: utcDateTime;
}
// tsp/main.tsp
import "@typespec/http";
import "@typespec/openapi3";
import "./common/base.tsp";
import "./common/scalars.tsp";
import "./common/ask.tsp";
import "./common/enums/lifecycle.tsp";
import "./common/enums/status.tsp";
import "./common/enums/log-level.tsp";
import "./domain/entities/feature.tsp";
import "./domain/entities/task.tsp";
import "./domain/entities/settings.tsp"; // NEW
import "./agents/analyze.tsp";
import "./agents/requirements.tsp";
@service({
title: "Shep AI CLI - Domain Models",
})
namespace ShepAI;
# Compile TypeSpec → Generate TypeScript + OpenAPI + JSON Schema
pnpm tsp:compile
# Verify generated output
cat src/domain/generated/output.ts | grep "export interface Settings"
// src/application/use-cases/settings/initialize-settings.use-case.ts
import type { Settings } from '@/domain/generated/output';
import type { ISettingsRepository } from '@/application/ports/output/settings.repository.interface';
export class InitializeSettingsUseCase {
constructor(private readonly settingsRepository: ISettingsRepository) {}
async execute(): Promise<Settings> {
// Check if settings already exist
const existing = await this.settingsRepository.load();
if (existing !== null) {
return existing;
}
// Create default settings (using generated type)
const defaults: Settings = {
id: 'singleton',
createdAt: new Date(),
updatedAt: new Date(),
models: {
analyze: 'claude-opus-4',
requirements: 'claude-sonnet-4',
plan: 'claude-sonnet-4',
implement: 'claude-sonnet-4',
},
user: {},
environment: {
defaultEditor: 'vim',
shellPreference: 'bash',
},
system: {
autoUpdate: true,
logLevel: 'info',
},
};
// Initialize in repository
await this.settingsRepository.initialize(defaults);
return defaults;
}
}
✅ Good: tsp/domain/entities/feature.tsp (one model)
✅ Good: tsp/domain/entities/task.tsp (one model)
❌ Bad: tsp/domain/entities.tsp (all models in one file)
/**
* Feature entity tracking work through SDLC lifecycle.
* Represents a unit of work from requirements to deployment.
*/
model Feature extends BaseEntity {
/** Human-readable feature name */
name: string;
/** Detailed feature description */
description: string;
/** Current SDLC lifecycle phase */
lifecycle: SdlcLifecycle;
/** Repository path this feature belongs to */
repoPath: string;
}
// Good: Enum for fixed set of values
enum SdlcLifecycle {
Requirements,
Plan,
Implementation,
Test,
Deploy,
Maintenance,
}
// Bad: String with no validation
model Feature {
lifecycle: string; // Could be anything!
}
model UserProfile {
// Optional fields with ?
name?: string;
email?: string;
// Required field (no ?)
createdAt: utcDateTime;
}
// Good: Extend BaseEntity for consistency
model Settings extends BaseEntity {
// Inherits: id, createdAt, updatedAt
models: ModelConfiguration;
}
// Bad: Duplicate fields
model Settings {
id: string;
createdAt: utcDateTime; // Duplicate!
updatedAt: utcDateTime; // Duplicate!
models: ModelConfiguration;
}
model BaseEntity {
/** Creation timestamp (RFC 3339 format) */
@encode(DateTimeKnownEncoding.rfc3339)
createdAt: utcDateTime;
}
// Generated TypeScript:
// createdAt: string; (ISO 8601 string)
model LegacyFeature {
name: string;
/** @deprecated Use 'description' instead */
@deprecated("Use 'description' instead")
summary: string;
description: string;
}
model Settings {
/** Default text editor
* @example "vim"
* @example "code"
*/
defaultEditor: string;
}
// tsp/domain/entities/settings.tsp
model Settings extends BaseEntity {
// ... existing fields ...
/** NEW: Telemetry opt-out flag */
telemetryEnabled: boolean = true; // Default value
}
Workflow:
.tsp filepnpm tsp:compile → Regenerate TypeScript.tsp and generated files// tsp/domain/entities/settings.tsp
model Settings extends BaseEntity {
// ... existing fields ...
// REMOVED: oldField: string; ← Delete this line
}
Workflow:
.tsp filepnpm tsp:compile → TypeScript compile errors appear// Before
model Settings {
editorPreference: string;
}
// After
model Settings {
defaultEditor: string;
}
Workflow:
@deprecatedpnpm tsp:compilepnpm tsp:compile again# Compile TypeSpec → Generate TypeScript + OpenAPI + JSON Schema
pnpm tsp:compile
# Format TypeSpec files with Prettier
pnpm tsp:format
# Watch mode (recompile on changes)
pnpm tsp:watch
# Validate without generating (dry run)
pnpm tsp:compile --no-emit
# Generate only OpenAPI (skip TypeScript)
pnpm tsp:compile --emit @typespec/openapi3
# Validate TypeSpec + Lint + Format (full check)
pnpm validate
Cause: Model name conflicts with existing type.
Solution: Rename the model or use namespace:
namespace Settings {
model Configuration {
// ...
}
}
Cause: Missing TypeSpec dependencies.
Solution:
pnpm install @typespec/compiler @typespec/http @typespec/openapi3 --save-dev
Cause: Cached compilation output.
Solution:
# Clear generated output
rm -rf apis/ src/domain/generated/
# Recompile
pnpm tsp:compile
Cause: Breaking change in domain model (expected behavior).
Solution:
TypeSpec compilation runs in CI pipeline:
# .github/workflows/ci.yml
jobs:
lint:
steps:
- name: Compile TypeSpec
run: pnpm tsp:compile
- name: Check for uncommitted changes
run: |
git diff --exit-code src/domain/generated/
IMPORTANT: Always commit generated files (src/domain/generated/output.ts) to version control. This ensures:
Update when:
Related files:
tsp/ - TypeSpec source filestspconfig.yaml - TypeSpec configurationpackage.json - TypeSpec dependencies and scripts