From f8247b1f9b098cc23efec30caec798be72d72d6a Mon Sep 17 00:00:00 2001 From: Toni Oriol Date: Mon, 28 Oct 2024 12:49:01 +0100 Subject: [PATCH] feat!: improved type safety for extra param BREAKING CHANGE: made the usage of fn or `to` exclusive. So they can't used both at the same time now. --- README.md | 139 ++++++++++++++++++++---------------- src/__tests__/index.test.ts | 11 +-- src/index.ts | 9 ++- src/types.ts | 31 ++++---- 4 files changed, 99 insertions(+), 91 deletions(-) diff --git a/README.md b/README.md index 60656f3..16b4c22 100644 --- a/README.md +++ b/README.md @@ -18,96 +18,89 @@ npm install transmutant ## Usage -### Basic Property Mapping +### Direct Property Mapping ```typescript -import { transmute } from 'transmutant'; - -interface User { - firstName: string; - lastName: string; - age: number; +interface Source { + email: string; } -interface UserDTO { - fullName: string; - yearOfBirth: number; +interface Target { + contactEmail: string; } -const schema = [ - { - to: 'fullName', - fn: ({ source }) => `${source.firstName} ${source.lastName}` - }, - { - to: 'yearOfBirth', - fn: ({ source }) => new Date().getFullYear() - source.age - } +const schema: Schema[] = [ + { from: 'email', to: 'contactEmail' } ]; -const user: User = { - firstName: 'John', - lastName: 'Doe', - age: 30 -}; +const source: Source = { email: 'john@example.com' }; +const target = transmute(schema, source); -const userDTO = transmute(schema, user); -// Result: { fullName: 'John Doe', yearOfBirth: 1994 } +// Result: { contactEmail: 'john@example.com' } ``` -### Direct Property Mapping +### Using Custom Transmutation Functions ```typescript interface Source { - id: number; - name: string; + firstName: string; + lastName: string; } interface Target { - userId: number; - userName: string; + fullName: string; } -const schema = [ - { from: 'id', to: 'userId' }, - { from: 'name', to: 'userName' } +const schema: Schema[] = [ + { + to: 'fullName', + fn: ({ source }) => `${source.firstName} ${source.lastName}` + } ]; -const source: Source = { id: 1, name: 'John' }; -const target = transmute(schema, source); -// Result: { userId: 1, userName: 'John' } +const source: Source = { firstName: 'John', lastName: 'Doe' }; +const target = transmute(schema, source); + +// Result: { fullName: 'John Doe' } ``` ### Using Extra Data ```typescript -interface Product { - price: number; +interface Source { + city: string; + country: string; +} + +interface Target { + location: string; } -interface PricedProduct { - finalPrice: number; +interface Extra { + separator: string; } -const schema = [ +const schema: Schema[] = [ { - to: 'finalPrice', - fn: ({ source, extra }) => source.price * (1 + extra?.taxRate) + to: 'location', + fn: ({ source, extra }) => + `${source.city}, ${source.country}${extra?.separator}` } ]; -const product: Product = { price: 100 }; -const pricedProduct = transmute( - schema, - product, - { taxRate: 0.2 } -); -// Result: { finalPrice: 120 } +const source: Source = { + city: 'New York', + country: 'USA' +}; + +const target = transmute(schema, source, { separator: ' | ' }); + +// Result: { location: 'New York, USA | ' } ``` ## API Reference -### `transmute(schema, source, extra?)` +### `transmute(schema, source, extra?)` Transmutes a source object into a target type based on the provided schema. @@ -117,24 +110,45 @@ Transmutes a source object into a target type based on the provided schema. - `source`: Source object to transmute - `extra`: (Optional) Additional data to pass to transmutation functions -#### Schema Options +#### Schema Types + +Each schema entry must specify the target property and use either direct mapping OR a custom function: -1. Direct mapping: ```typescript -{ +type Schema = { + /** Target property key */ to: keyof Target; - from: keyof Source; -} +} & ( + | { + /** Source property key for direct mapping */ + from: keyof Source; + fn?: never; + } + | { + /** Custom transmutation function */ + fn: TransmuteFn; + from?: never; + } +); ``` -2. Custom transmutation: +The `TransmuteFn` type is defined as: ```typescript -{ - to: keyof Target; - fn: (args: { source: Source; extra?: Extra }) => unknown; -} +type TransmuteFn = (args: { + source: Source; + extra?: TExtra; +}) => unknown; ``` +#### Behavior Notes + +- Direct mapping uses the `from` property to copy values directly from source to target +- Custom functions receive the entire source object and optional extra data +- If a direct mapping property is undefined or null, it will be set to `null` in the target object +- Empty schemas result in an empty object +- Each schema entry must use either `from` OR `fn`, but not both +- The schema is processed sequentially, with each rule contributing to the final object + ## License MIT @@ -142,4 +156,3 @@ MIT ## Contributing Contributions are welcome! Please feel free to submit a Pull Request. - diff --git a/src/__tests__/index.test.ts b/src/__tests__/index.test.ts index 43622d3..584dfb2 100644 --- a/src/__tests__/index.test.ts +++ b/src/__tests__/index.test.ts @@ -34,7 +34,7 @@ describe('transmute', () => { } it('should perform direct property mapping', () => { - const schema: Schema>[] = [ + const schema: Schema[] = [ { from: 'email', to: 'contactEmail' } ] @@ -43,7 +43,7 @@ describe('transmute', () => { }) it('should handle custom transmutation functions', () => { - const schema: Schema>[] = [ + const schema: Schema[] = [ { to: 'fullName', fn: ({ source }) => `${source.firstName} ${source.lastName}` @@ -55,7 +55,7 @@ describe('transmute', () => { }) it('should handle transmutation with both "from" and "fn"', () => { - const schema: Schema>[] = [ + const schema: Schema[] = [ { to: 'userAge', fn: ({ source }) => source['age'] + 1 @@ -67,7 +67,10 @@ describe('transmute', () => { }) it('should handle extra data in transmutations', () => { - const schema: Schema>[] = [ + interface Extra { + 'separator': string + } + const schema: Schema[] = [ { to: 'location', fn: ({ source, extra }) => diff --git a/src/index.ts b/src/index.ts index 8c3c8b5..14d5f0a 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,4 +1,4 @@ -import { Extra, Schema } from './types' +import { Schema } from './types' export * from './types' @@ -12,16 +12,15 @@ export * from './types' * @param extra - Optional extra data to pass to mutation functions * @returns Transmuted object matching Target type */ -export const transmute = ( - schema: Schema[], +export const transmute = ( + schema: Schema[], source: Source, extra?: TExtra ): Target => schema.reduce( (acc, { from, to, fn }) => ({ ...acc, - [to]: fn ? fn({ source, from, extra }) : from && source[from] ? source[from] : null + [to]: fn ? fn({ source, extra }) : source[from] ?? null }), {} as Target ) - diff --git a/src/types.ts b/src/types.ts index 30ab802..d05b7ca 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,44 +1,37 @@ -/** - * Represents additional data that can be passed to mutation functions - */ -export type Extra = Record - /** * Arguments passed to a mutation function * @template From - The source type being transmuted from */ -export interface TransmuteFnArgs { +export type TransmuteFnArgs = { /** The source object being transmuted */ source: Source - /** Optional source property key */ - from?: keyof Source /** Optional extra data to assist with transmutation */ - extra?: Extra + extra?: TExtra } /** * Function that performs a custom transmutation on a source object * @template From - The source type being transmuted from */ -export type TransmuteFn = (args: TransmuteFnArgs) => unknown +export type TransmuteFn = (args: TransmuteFnArgs) => unknown /** * Defines how a property should be transmuted from source to target type * @template From - The source type being transmuted from * @template To - The target type being transmuted to */ -export type Schema = | { +export type Schema = { /** Target property key */ to: keyof Target +} & ( + | { /** Source property key for direct mapping */ from: keyof Source + fn?: never +} + | { /** Custom transmutation function */ - fn?: TransmuteFn -} | { - /** Target property key */ - to: keyof Target - /** Source property key for direct mapping */ - from?: keyof Source - /** Custom transmutation function */ - fn: TransmuteFn + fn: TransmuteFn + from?: never } + )