Skip to content

Commit

Permalink
feat!: improved type safety for extra param
Browse files Browse the repository at this point in the history
BREAKING CHANGE: made the usage of fn or `to` exclusive. So they can't used both at the same time now.
  • Loading branch information
tonioriol committed Oct 28, 2024
1 parent d3259a6 commit f8247b1
Show file tree
Hide file tree
Showing 4 changed files with 99 additions and 91 deletions.
139 changes: 76 additions & 63 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<Source, Target>[] = [
{ from: 'email', to: 'contactEmail' }
];

const user: User = {
firstName: 'John',
lastName: 'Doe',
age: 30
};
const source: Source = { email: '[email protected]' };
const target = transmute(schema, source);

const userDTO = transmute<User, UserDTO>(schema, user);
// Result: { fullName: 'John Doe', yearOfBirth: 1994 }
// Result: { contactEmail: '[email protected]' }
```

### 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<Source, Target>[] = [
{
to: 'fullName',
fn: ({ source }) => `${source.firstName} ${source.lastName}`
}
];

const source: Source = { id: 1, name: 'John' };
const target = transmute<Source, Target>(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<Source, Target, Extra>[] = [
{
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<Product, PricedProduct>(
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<Source, Target>(schema, source, extra?)`
### `transmute(schema, source, extra?)`

Transmutes a source object into a target type based on the provided schema.

Expand All @@ -117,29 +110,49 @@ 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<Source, Target, TExtra> = {
/** 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<Source, TExtra>;
from?: never;
}
);
```

2. Custom transmutation:
The `TransmuteFn` type is defined as:
```typescript
{
to: keyof Target;
fn: (args: { source: Source; extra?: Extra }) => unknown;
}
type TransmuteFn<Source, TExtra> = (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

## Contributing

Contributions are welcome! Please feel free to submit a Pull Request.

11 changes: 7 additions & 4 deletions src/__tests__/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ describe('transmute', () => {
}

it('should perform direct property mapping', () => {
const schema: Schema<SourceUser, Pick<TargetUser, 'contactEmail'>>[] = [
const schema: Schema<SourceUser, TargetUser>[] = [
{ from: 'email', to: 'contactEmail' }
]

Expand All @@ -43,7 +43,7 @@ describe('transmute', () => {
})

it('should handle custom transmutation functions', () => {
const schema: Schema<SourceUser, Pick<TargetUser, 'fullName'>>[] = [
const schema: Schema<SourceUser, TargetUser>[] = [
{
to: 'fullName',
fn: ({ source }) => `${source.firstName} ${source.lastName}`
Expand All @@ -55,7 +55,7 @@ describe('transmute', () => {
})

it('should handle transmutation with both "from" and "fn"', () => {
const schema: Schema<SourceUser, Pick<TargetUser, 'userAge'>>[] = [
const schema: Schema<SourceUser, TargetUser>[] = [
{
to: 'userAge',
fn: ({ source }) => source['age'] + 1
Expand All @@ -67,7 +67,10 @@ describe('transmute', () => {
})

it('should handle extra data in transmutations', () => {
const schema: Schema<SourceUser, Pick<TargetUser, 'location'>>[] = [
interface Extra {
'separator': string
}
const schema: Schema<SourceUser, TargetUser, Extra>[] = [
{
to: 'location',
fn: ({ source, extra }) =>
Expand Down
9 changes: 4 additions & 5 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Extra, Schema } from './types'
import { Schema } from './types'

export * from './types'

Expand All @@ -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 = <Source, Target, TExtra extends Extra = Extra>(
schema: Schema<Source, Target>[],
export const transmute = <Source, Target, TExtra = unknown>(
schema: Schema<Source, Target, TExtra>[],
source: Source,
extra?: TExtra
): Target =>
schema.reduce<Target>(
(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
)

31 changes: 12 additions & 19 deletions src/types.ts
Original file line number Diff line number Diff line change
@@ -1,44 +1,37 @@
/**
* Represents additional data that can be passed to mutation functions
*/
export type Extra = Record<string, unknown>

/**
* Arguments passed to a mutation function
* @template From - The source type being transmuted from
*/
export interface TransmuteFnArgs<Source> {
export type TransmuteFnArgs<Source, TExtra> = {
/** 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<Source> = (args: TransmuteFnArgs<Source>) => unknown
export type TransmuteFn<Source, TExtra = unknown> = (args: TransmuteFnArgs<Source, TExtra>) => 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<Source, Target> = | {
export type Schema<Source, Target, TExtra = unknown> = {
/** Target property key */
to: keyof Target
} & (
| {
/** Source property key for direct mapping */
from: keyof Source
fn?: never
}
| {
/** Custom transmutation function */
fn?: TransmuteFn<Source>
} | {
/** Target property key */
to: keyof Target
/** Source property key for direct mapping */
from?: keyof Source
/** Custom transmutation function */
fn: TransmuteFn<Source>
fn: TransmuteFn<Source, TExtra>
from?: never
}
)

0 comments on commit f8247b1

Please sign in to comment.