Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
141 changes: 141 additions & 0 deletions AI.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
# AI Skill: Type-Safe Mapping

This package provides a utility for mapping one TypeScript object to another with 100% type
safety — zero runtime overhead, zero manual type definitions, zero duplication.

## When to Use

- When converting an **API response (DTO)** to a **Domain Model** (e.g., `snake_case` → `camelCase`)
- When mapping **database rows** to application objects
- When normalising data from **third-party integrations**
- When you need TypeScript to **break the build** if a new field is added but the mapper is not
updated

## Quick Start for Agents

### Step 1 — Scaffold the mapper with the CLI

```bash
npx @kylebrodeur/type-safe-mapping init ./src/types/ApiUser.ts ./src/models/User.ts
```

This scans both TypeScript files, auto-matches fields where possible, and writes a ready-to-edit
`ApiUserMapper.ts` next to the source file.

Use `--stdout` to preview the generated code in the terminal without writing a file:

```bash
npx @kylebrodeur/type-safe-mapping init ./src/types/ApiUser.ts ./src/models/User.ts --stdout
```

Use `--out <path>` to write to a specific location:

```bash
npx @kylebrodeur/type-safe-mapping init ./src/types/ApiUser.ts ./src/models/User.ts --out ./src/mappers/UserMapper.ts
```

### Step 2 — Review and complete the generated file

The scaffolded file looks like this:

```typescript
import { MappedServiceBase, MappedType } from '@kylebrodeur/type-safe-mapping';
import type { ApiUser } from './ApiUser.js';

const fieldMapping = {
// TODO: user_id: 'targetFieldName', // fill these in
first_name: 'firstName', // auto-matched by camelCase
last_name: 'lastName', // auto-matched by camelCase
} as const;

export type User = MappedType<ApiUser, typeof fieldMapping>;

export class ApiUserMapper extends MappedServiceBase<ApiUser, typeof fieldMapping> {
protected fieldMapping = fieldMapping;
}
```

Fill in any `TODO` entries with the correct target field names.

### Step 3 — Validate type safety

```bash
npx tsc --noEmit
```

TypeScript will report an error if any mapped fields are missing or mismatched.

## Usage Pattern (Library API)

```typescript
import { MappedServiceBase, MappedType } from '@kylebrodeur/type-safe-mapping';

// 1. Source type — must include an index signature
interface ApiUser {
user_id: string;
first_name: string;
last_name: string;
[key: string]: unknown; // ← required
}

// 2. Field mapping with `as const` — external → internal
const userMapping = {
user_id: 'id',
first_name: 'firstName',
last_name: 'lastName',
Comment on lines +73 to +85
Copy link

Copilot AI Mar 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This section states the source type “must include an index signature” and labels it “required”, but the library’s public constraint is TSource extends Record<string, unknown>, which does not require an index signature (object types with explicit properties satisfy it). This guidance is likely to confuse users and should be corrected (or the library constraint changed if an index signature is truly required).

Suggested change
// 1. Source type — must include an index signature
interface ApiUser {
user_id: string;
first_name: string;
last_name: string;
[key: string]: unknown; // ← required
}
// 2. Field mapping with `as const` — external → internal
const userMapping = {
user_id: 'id',
first_name: 'firstName',
last_name: 'lastName',
// 1. Source type — plain object type compatible with Record<string, unknown>
interface ApiUser {
user_id: string;
first_name: string;
last_name: string;
}
// 2. Field mapping with `as const` — external → internal
const userMapping = {
user_id: 'id',
first_name: 'firstName',
lastName: 'lastName',

Copilot uses AI. Check for mistakes.
} as const;

// 3. Infer the domain type automatically
type User = MappedType<ApiUser, typeof userMapping>;
// { id: string; firstName: string; lastName: string; }

// 4. Create the mapper class
class UserMapper extends MappedServiceBase<ApiUser, typeof userMapping> {
protected fieldMapping = userMapping;
}

// 5. Transform data
const mapper = new UserMapper();
const user = mapper.map({ user_id: '1', first_name: 'Alice', last_name: 'Smith' });
// → { id: '1', firstName: 'Alice', lastName: 'Smith' }

const apiRow = mapper.reverseMap({ id: '1', firstName: 'Alice', lastName: 'Smith' });
// → { user_id: '1', first_name: 'Alice', last_name: 'Smith' }
```

## Common Pitfalls

| Problem | Fix |
|---|---|
| TypeScript errors about index signature | Add `[key: string]: unknown` to your source interface |
| Type inference fails | Make sure you used `as const` on the field mapping object |
| Unmapped fields appear in output | Only fields in `fieldMapping` are included — this is intentional |
| CLI says "no interfaces found" | Check that the file contains a `interface Foo { ... }` declaration |
Comment on lines +108 to +113
Copy link

Copilot AI Mar 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The “Common Pitfalls” table rows start with || (double pipe), which creates an extra empty column in standard markdown rendering. Use single leading pipes for proper table formatting.

Copilot uses AI. Check for mistakes.

## CLI Reference

```
npx @kylebrodeur/type-safe-mapping init <source-file> [target-file] [options]

Arguments:
source-file TypeScript file with the source interface (e.g. API response)
target-file TypeScript file with the target/domain interface (optional)

Options:
--stdout Print generated code to stdout (great for AI agent inspection)
--out <file> Write to a specific output file path
```

## Exports

```typescript
export { MappedServiceBase } from './MappedServiceBase';
export type { MappedType, MappingDefinition, ReverseMapping } from './types';
Comment on lines +132 to +133
Copy link

Copilot AI Mar 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The “Exports” snippet shows extensionless internal paths ('./MappedServiceBase', './types'), but this package is ESM/NodeNext and the actual source exports use .js extensions. Consider either showing the real src/index.ts exports (with .js) or replacing this section with consumer-facing import examples from the package entrypoint to avoid misleading copy/paste.

Suggested change
export { MappedServiceBase } from './MappedServiceBase';
export type { MappedType, MappingDefinition, ReverseMapping } from './types';
import {
MappedServiceBase,
type MappedType,
type MappingDefinition,
type ReverseMapping,
} from '@kylebrodeur/type-safe-mapping';

Copilot uses AI. Check for mistakes.
```

| Export | Kind | Purpose |
|---|---|---|
| `MappedServiceBase` | Class | Base class to extend for your mappers |
| `MappedType<TSource, M>` | Type | Infers the domain type from a mapping |
| `MappingDefinition<TSource>` | Type | Constraint for valid field mapping objects |
| `ReverseMapping<TSource, M, Key>` | Type | Internal utility for reverse lookup |
57 changes: 57 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ Transform data between different shapes (API ↔ Domain) without writing repetit

- [Why?](#why)
- [Quick Start](#quick-start)
- [CLI](#cli)
- [Key Features](#key-features)
- [Use Cases](#use-cases)
- [API Reference](#api-reference)
Expand Down Expand Up @@ -90,6 +91,62 @@ const api = mapper.reverseMap({ isEnterprise: false, commerceType: 'B2C' });
- **Bidirectional**: Map from external → internal and internal → external
- **Optional Fields**: Handles optional values correctly in both directions
- **Zero Dependencies**: No runtime dependencies
- **AI-Ready CLI**: Generate mapper boilerplate from existing TypeScript interfaces

## CLI

Generate a type-safe mapper from two existing TypeScript interface files:

```bash
npx @kylebrodeur/type-safe-mapping init <source-file> <target-file>
```

The CLI reads both files, auto-matches fields where possible (e.g. `first_name` → `firstName`),
and writes a ready-to-edit mapper file.

### Options

| Flag | Description |
|------|-------------|
| `--stdout` | Print generated code to stdout instead of writing a file |
| `--out <path>` | Write the output to a specific file path |

Comment on lines +109 to +113
Copy link

Copilot AI Mar 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The markdown tables in this section start rows with || (double pipe), which renders as an extra empty column in most markdown renderers. Use single leading pipes (| Flag | Description |, etc.) for valid tables.

Copilot uses AI. Check for mistakes.
### Examples

```bash
# Generate ApiUserMapper.ts next to ApiUser.ts
npx @kylebrodeur/type-safe-mapping init ./src/types/ApiUser.ts ./src/models/User.ts

# Preview without writing (great for AI agents)
npx @kylebrodeur/type-safe-mapping init ./src/types/ApiUser.ts ./src/models/User.ts --stdout

# Generate from source only (all fields as TODOs)
npx @kylebrodeur/type-safe-mapping init ./src/types/ApiUser.ts --stdout

# Write to a specific path
npx @kylebrodeur/type-safe-mapping init ./src/types/ApiUser.ts ./src/models/User.ts --out ./src/mappers/UserMapper.ts
```

### Generated Output

```typescript
import { MappedServiceBase, MappedType } from '@kylebrodeur/type-safe-mapping';
import type { ApiUser } from './ApiUser.js';

const fieldMapping = {
// TODO: user_id: 'targetFieldName',
first_name: 'firstName',
last_name: 'lastName',
} as const;

export type User = MappedType<ApiUser, typeof fieldMapping>;

export class ApiUserMapper extends MappedServiceBase<ApiUser, typeof fieldMapping> {
protected fieldMapping = fieldMapping;
}
```

Fill in any `TODO` entries and run `tsc --noEmit` to verify type safety.

## API Reference

Expand Down
Loading