-
Notifications
You must be signed in to change notification settings - Fork 0
feat: add AI-first CLI and AI.md agent manual #3
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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', | ||||||||||||||||||
| } 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
|
||||||||||||||||||
|
|
||||||||||||||||||
| ## 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
|
||||||||||||||||||
| export { MappedServiceBase } from './MappedServiceBase'; | |
| export type { MappedType, MappingDefinition, ReverseMapping } from './types'; | |
| import { | |
| MappedServiceBase, | |
| type MappedType, | |
| type MappingDefinition, | |
| type ReverseMapping, | |
| } from '@kylebrodeur/type-safe-mapping'; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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) | ||
|
|
@@ -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
|
||
| ### 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 | ||
|
|
||
|
|
||
There was a problem hiding this comment.
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).