Skip to content

feat: dynamic mapping expansions + fix CI build failures#2

Draft
Copilot wants to merge 3 commits intomainfrom
copilot/review-dynamic-mapping-enhancement
Draft

feat: dynamic mapping expansions + fix CI build failures#2
Copilot wants to merge 3 commits intomainfrom
copilot/review-dynamic-mapping-enhancement

Conversation

Copy link

Copilot AI commented Mar 8, 2026

Implements proposal #2 (Dynamic Mapping Expansions) and resolves the broken standalone build caused by a missing monorepo-root tsconfig.base.json.

Description

Build fix: tsconfig.json referenced ../../tsconfig.base.json which doesn't exist outside the original monorepo. Compiler options are now inlined. devDependencies (typescript, vitest, eslint, @typescript-eslint/*) added to package.json so the package is fully self-contained.

Dynamic Mapping Expansions: map() and reverseMap() now accept an optional MapOptions second argument. When { dynamicMapping: true } is set, fields absent from the mapping are passed through unchanged instead of being silently dropped — useful when only part of the incoming schema is known upfront.

const mapper = new UserMapper();

mapper.map({ custom_a: true, other_field: 'dynamic_value' });
// { isEnterprise: true }  ← other_field dropped (default)

mapper.map({ custom_a: true, other_field: 'dynamic_value' }, { dynamicMapping: true });
// { isEnterprise: true, other_field: 'dynamic_value' }  ← passed through

TypeScript overloads keep the return type narrow (MappedType<TSource, TMapping>) for the common case and widen to & Record<string, unknown> only when dynamicMapping: true is explicit — zero impact on existing call sites.

Type of change

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • Documentation update

Checklist

  • My code follows the style guidelines of this project
  • I have performed a self-review of my own code
  • I have commented my code, particularly in hard-to-understand areas
  • I have made corresponding changes to the documentation
  • My changes generate no new warnings
  • I have added tests that prove my fix is effective or that my feature works
  • New and existing unit tests pass locally with my changes
  • Any dependent changes have been merged and published

Additional Notes

  • MapOptions is now a public export from the package entrypoint.
  • 9 new tests cover runtime pass-through behaviour, undefined field skipping, default (non-dynamic) behaviour, and expectTypeOf assertions for both overload branches.
  • The [key: string]: unknown index signature has been added to the ApiRow test interface (required to pass dynamic fields into the mapper).
Original prompt

Read the following proposals. Take into account other branches and don't duplicate work. Create a plan for review after seeing if this is good idea.

Here’s a recap of the other enhancement proposals for @kylebrodeur/type-safe-mapping, along with their goals, benefits, and implementation considerations:


1. Runtime Validation for Field Mappings (Already discussed)

  • Core Idea: Add lightweight, internal validation for mappings (and possibly allow external integrations like Zod).
  • Status: Discussed in detail.

2. Dynamic Mapping Expansions

Description:

  • Enable support for optional unmapped fields or dynamic schema expansions, where not all fields are defined upfront but should still be carried over during mappings.
  • Useful for cases where input data may contain additional fields that aren’t explicitly accounted for but must still be included in the output.

Example:

const fieldMapping = {
  custom_a: 'isEnterprise',
} as const;

const data = {
  custom_a: true,
  other_field: 'dynamic_value', // Not defined in the direct mapping
};

const result = mapper.map(data, { dynamicMapping: true });
// Output: { isEnterprise: true, other_field: 'dynamic_value' }

Benefits:

  1. Avoids data truncation in scenarios where the input includes unknown but meaningful fields.
  2. Simplifies working with APIs or datasets where schemas are only partially known.

Implementation:

  • Introduce a dynamicMapping option to map() and reverseMap().
  • Copy all unmapped fields into the resulting object when this option is enabled.

Breaking Changes:

  • None, as this behavior would be explicitly enabled via the dynamicMapping flag.

3. Support for Key-Value Field Type Validation

Description:

  • Extend the fieldMapping structure to include field-level options such as the expected data type, default values, or optionality, similar to schema validators.

Example:

const fieldMapping = {
  custom_a: { target: 'isEnterprise', type: 'boolean' },
  custom_b: { target: 'commerceType', type: 'string', optional: true },
} as const;

mapper.map({
  custom_a: true,
  custom_b: 123, // Throws: Type mismatch
});

Benefits:

  1. Allows developers to define the behavior of each mapping field more explicitly.
  2. Provides fine-grained control over typing, validation, and optionality at the field level.

Implementation:

  • Update the fieldMapping structure to accept objects instead of string mappings.
  • Extend the MappedType utility to infer the target type from this enhanced mapping structure.

Breaking Changes:

  • Significant if the fieldMapping structure fundamentally changes.

4. TypeSafeMap Utility

Description:

  • Add a TypeSafeMap class that implements stricter type guarantees for key-value mappings. This complements the schema-based mapping functionality for developers working with dynamic maps.

Example:

const map = new TypeSafeMap<string, number>();

map.set('keyA', 123);  // Works
map.set('keyB', 'Wrong Type'); // Throws

Benefits:

  1. Provides the developer with a strict runtime alternative to Map<K, V>.
  2. Gives tighter control over untyped Maps in dynamic scenarios.

Implementation:

  • Implement a wrapper around the native JavaScript Map to validate types on get and set operations.
  • Optionally integrate with fieldMapping for predefining key-value constraints.

Breaking Changes:

  • None, as this would be an additive feature.

5. Enhanced Error Messages and Logging

Description:

  • Provide more detailed and descriptive error messages during runtime.
  • Offer an optional debugging or logging mode for developers to trace field-mapping errors.

Benefits:

  1. Makes debugging field mappings significantly easier.
  2. Reduces developer frustration caused by vague or incomplete error messages.

Example Error Message:

Error: Failed to map field `custom_a`.
  - Expected: boolean
  - Received: string

Implementation:

  • Add field path and expected vs. received values to error messages.
  • Introduce a debug: true option to map() for tracing workflows.

Breaking Changes:

  • None. However, changes to error messages may affect integrations relying on specific error formats.

6. Extend MappedServiceBase for Multi-Layer Mappings

Description:

  • Add advanced features that support combining mappings or validating entire data pipelines using multiple layers of transformations.

Example:

const mapping1 = {
  user_id: 'id',
  user_name: 'name',
} as const;

const mapping2 = {
  address: 'userAddress',
  phone: 'userPhone',
} as const;

const combinedMapping = MappedServiceBase.combineMappings(mapping1, mapping2);
// Combined result:
// { user_id: 'id', user_name: 'name', address: 'userAddress', phone: 'userPhone' }

Benefits:

  1. Allows complex data ...

💡 You can make Copilot smarter by setting up custom instructions, customizing its development environment and configuring Model Context Protocol (MCP) servers. Learn more Copilot coding agent tips in the docs.

Copilot AI and others added 2 commits March 8, 2026 23:48
…rrors

Co-authored-by: kylebrodeur <811103+kylebrodeur@users.noreply.github.com>
Co-authored-by: kylebrodeur <811103+kylebrodeur@users.noreply.github.com>
Copilot AI changed the title [WIP] Plan review for dynamic mapping expansions feat: dynamic mapping expansions + fix CI build failures Mar 8, 2026
@kylebrodeur kylebrodeur requested a review from Copilot March 9, 2026 00:33
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Adds an opt-in “dynamic mapping” mode to preserve unmapped fields during map()/reverseMap() operations, and updates project configuration so the package can build/test outside the original monorepo.

Changes:

  • Inline TypeScript compiler options in tsconfig.json (removing missing monorepo base config dependency).
  • Add MapOptions + overloads for map()/reverseMap() to support { dynamicMapping: true } pass-through behavior.
  • Expand documentation, changelog entries, and tests for the new option.

Reviewed changes

Copilot reviewed 7 out of 9 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
tsconfig.json Removes monorepo extends and inlines compiler options for standalone builds.
src/MappedServiceBase.ts Implements dynamicMapping option with overloads for typed return widening.
src/types.ts Adds exported MapOptions type describing mapping options.
src/index.ts Exports MapOptions from the package entrypoint and documents the new option.
tests/mapping.test.ts Adds runtime tests and type assertions around dynamic mapping behavior.
README.md Documents dynamic mapping feature and updates API reference accordingly.
CHANGELOG.md Adds an Unreleased entry describing dynamic mapping + build/tooling fixes.
package.json Adds dev tooling dependencies and formats files array.
package-lock.json Locks the new dev dependency tree.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

});

it('reverseMap() does not include unmapped fields by default', () => {
const result = mapper.reverseMap({ isEnterprise: false, commerceType: 'B2C', extra_info: 42 });
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 test calls reverseMap() with an object literal containing extra_info but without { dynamicMapping: true }. With the new overloads, the default reverseMap() signature only accepts Partial<MappedType<...>>, so this won’t type-check due to excess property checks. Consider casting the input in the test (or constructing it in a way that avoids excess property checks) to reflect the intended JS/runtime behavior being tested.

Suggested change
const result = mapper.reverseMap({ isEnterprise: false, commerceType: 'B2C', extra_info: 42 });
const input = { isEnterprise: false, commerceType: 'B2C', extra_info: 42 };
const result = mapper.reverseMap(input);

Copilot uses AI. Check for mistakes.
Comment on lines +38 to +42
if (options?.dynamicMapping) {
for (const [key, value] of Object.entries(source)) {
if (!mappedExternalKeys.has(key) && value !== undefined) {
result[key] = value;
}
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.

When dynamicMapping is enabled, the pass-through loop can overwrite keys already produced by the explicit mapping (e.g., if the source contains an unmapped key that happens to match an internal mapped key). This can silently change mapped values. Consider preventing pass-through from overwriting any key that already exists in result (or define/implement a clear precedence rule, with mapped fields typically winning).

Copilot uses AI. Check for mistakes.

if (options?.dynamicMapping) {
for (const [key, value] of Object.entries(target)) {
if (!mappedInternalKeys.has(key) && value !== undefined) {
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.

Same overwrite risk in reverseMap(): with dynamicMapping enabled, pass-through keys from target can collide with already-mapped external keys and overwrite the mapped output. Consider skipping pass-through for any key that is already present in result (or otherwise ensure mapped values can’t be overridden).

Suggested change
if (!mappedInternalKeys.has(key) && value !== undefined) {
if (!mappedInternalKeys.has(key) && !(key in result) && value !== undefined) {

Copilot uses AI. Check for mistakes.
Comment on lines 54 to +62
"engines": {
"node": ">=18.0.0"
},
"devDependencies": {
"@typescript-eslint/eslint-plugin": "^8.56.1",
"@typescript-eslint/parser": "^8.56.1",
"eslint": "^10.0.3",
"typescript": "^5.9.3",
"vitest": "^4.0.18"
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.

engines.node is set to ">=18.0.0", but the newly added dev tooling (via transitive deps) requires newer Node versions (e.g., ESLint 10 requires Node ^20.19.0 and Vitest 4 requires Node ^20). This will break installs/CI on Node 18. Either bump the package’s Node engine to match (e.g., >=20) or pin eslint/vitest/@typescript-eslint to versions that support Node 18.

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants