Skip to content

Commit

Permalink
feat!: enforce type compatibility in schema mappings
Browse files Browse the repository at this point in the history
- Add ValidSourceKey helper type to validate source/target property types
- Restrict direct property mappings to compatible types only
- Keep TransmuteFn functionality unchanged
- Add example usage demonstrating type checking

BREAKING CHANGE: Schema type now enforces strict type compatibility between
source and target properties. Code with mismatched types in direct property
mappings will need to be updated to use TransmuteFn for type conversion or
fix the type mismatch.
  • Loading branch information
tonioriol committed Oct 31, 2024
1 parent 2525084 commit cc26b94
Show file tree
Hide file tree
Showing 5 changed files with 66 additions and 96 deletions.
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

### Features

* enforce strict return types in transformation functions ([bb575df](https://github.com/tonioriol/transmutant/commit/bb575dfd605934d76627867ed507567591c86317))
* enforce strict return types in transmutation functions ([bb575df](https://github.com/tonioriol/transmutant/commit/bb575dfd605934d76627867ed507567591c86317))

## [2.0.0](https://github.com/tonioriol/transmutant/compare/v1.0.1...v2.0.0) (2024-10-28)

Expand Down
68 changes: 34 additions & 34 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# 🧬 Transmutant 🧬

A powerful, type-safe TypeScript library for transforming objects through flexible schema definitions.
A powerful, type-safe TypeScript library for transmuting objects through flexible schema definitions.

[![npm version](https://badge.fury.io/js/transmutant.svg)](https://www.npmjs.com/package/transmutant)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
Expand All @@ -10,9 +10,9 @@ A powerful, type-safe TypeScript library for transforming objects through flexib
## Features

- 🔒 **Type-safe**: Full TypeScript support with strong type inference
- 🎯 **Flexible mapping**: Direct property mapping or custom transformation functions
- 🎯 **Flexible mapping**: Direct property mapping or custom transmutation functions
-**High performance**: Minimal overhead and zero dependencies
- 🔄 **Extensible**: Support for custom transformation logic and external data
- 🔄 **Extensible**: Support for custom transmutation logic and external data
- 📦 **Lightweight**: Zero dependencies, small bundle size
- 🛠️ **Predictable**: Transparent handling of undefined values

Expand Down Expand Up @@ -40,7 +40,7 @@ interface UserDTO {
contactEmail: string;
}

// Define transformation schema
// Define transmutation schema
const schema: Schema<User, UserDTO>[] = [
{
to: 'fullName',
Expand All @@ -52,7 +52,7 @@ const schema: Schema<User, UserDTO>[] = [
}
];

// Transform the object
// Transmut the object
const user: User = {
firstName: 'John',
lastName: 'Doe',
Expand All @@ -67,20 +67,20 @@ const userDTO = transmute(schema, user);

### Schema Definition

A schema is an array of transformation rules that define how properties should be mapped from the source to the target type. Each rule specifies the target property key and either a source property key for direct mapping or a transformation function that produces the correct type for that target property.
A schema is an array of transmutation rules that define how properties should be mapped from the source to the target type. Each rule specifies the target property key and either a source property key for direct mapping or a transmutation function that produces the correct type for that target property.

```typescript
type Schema<Source, Target, TExtra = unknown> = {
type Schema<Source, Target, Extra = unknown> = {
[TargetKey in keyof Target]: {
/** Target property key */
to: TargetKey
/** Source property key for direct mapping or a custom transformation function */
from: keyof Source | TransmuteFn<Source, Target, TargetKey, TExtra>
/** Source property key for direct mapping or a custom transmutation function */
from: keyof Source | TransmuteFn<Source, Target, TargetKey, Extra>
}
}[keyof Target]
```
### Transformation Types
### Transmutation Types
#### 1. Direct Property Mapping
Expand All @@ -100,9 +100,9 @@ const schema: Schema<Source, Target>[] = [
];
```

#### 2. Custom Transformation Functions
#### 2. Custom Transmutation Functions

Transform properties using custom logic with type safety:
Transmute properties using custom logic with type safety:

```typescript
interface Source {
Expand All @@ -121,9 +121,9 @@ const schema: Schema<Source, Target>[] = [
];
```

#### 3. External Data Transformations
#### 3. External Data Transmutations

Include additional context in transformations:
Include additional context in transmutations:

```typescript
interface Source {
Expand All @@ -149,7 +149,7 @@ const schema: Schema<Source, Target, ExtraData>[] = [

### Handling Undefined Values

When a source property doesn't exist or a transformation function returns undefined, the target property will remain undefined:
When a source property doesn't exist or a transmutation function returns undefined, the target property will remain undefined:

```typescript
interface Source {
Expand All @@ -168,7 +168,7 @@ const schema: Schema<Source, Target>[] = [
},
{
to: 'computedField',
from: ({ source }) => undefined // Transformation returns undefined
from: ({ source }) => undefined // Transmutation returns undefined
}
];

Expand All @@ -179,21 +179,21 @@ const result = transmute(schema, { existingField: 'value' });
This allows you to:
- Distinguish between unset values (`undefined`) and explicitly set null values
- Handle optional properties naturally
- Process partial transformations as needed
- Process partial transmutations as needed

## API Reference

### `transmute<Source, Target, TExtra = unknown>`
### `transmute<Source, Target, Extra = unknown>`

Main transformation function.
Main transmutation function.

#### Parameters

| Parameter | Type | Description |
|-----------|------------------------------------|-------------------------------|
| schema | `Schema<Source, Target, TExtra>[]` | Array of transformation rules |
| source | `Source` | Source object to transform |
| extra? | `TExtra` | Optional additional data |
| Parameter | Type | Description |
|-----------|-----------------------------------|------------------------------|
| schema | `Schema<Source, Target, Extra>[]` | Array of transmutation rules |
| source | `Source` | Source object to transmut |
| extra? | `Extra` | Optional additional data |

#### Returns

Expand All @@ -203,29 +203,29 @@ Returns an object of type `Target`.

```typescript
/**
* Schema entry defining how a property should be transformed
* Schema entry defining how a property should be transmuted
*/
type Schema<Source, Target, TExtra = unknown> = {
type Schema<Source, Target, Extra = unknown> = {
[TargetKey in keyof Target]: {
/** Target property key */
to: TargetKey
/** Source property key for direct mapping or a custom transformation function */
from: keyof Source | TransmuteFn<Source, Target, TargetKey, TExtra>
/** Source property key for direct mapping or a custom transmutation function */
from: keyof Source | TransmuteFn<Source, Target, TargetKey, Extra>
}
}[keyof Target]

/**
* Function that performs property transformation
* Function that performs property transmutation
*/
type TransmuteFn<Source, Target, TargetKey extends keyof Target, TExtra = unknown> =
(args: TransmuteFnArgs<Source, TExtra>) => Target[TargetKey]
type TransmuteFn<Source, Target, TargetKey extends keyof Target, Extra = unknown> =
(args: TransmuteFnArgs<Source, Extra>) => Target[TargetKey]

/**
* Arguments passed to transformation function
* Arguments passed to transmutation function
*/
type TransmuteFnArgs<Source, TExtra> = {
type TransmuteFnArgs<Source, Extra> = {
source: Source
extra?: TExtra
extra?: Extra
}
```
Expand Down
59 changes: 11 additions & 48 deletions src/__tests__/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,31 +47,26 @@ describe('transmute', () => {
{
to: 'fullName',
from: ({ source }) => `${source.firstName} ${source.lastName}`
}
]

const result = transmute(schema, sourceUser)
expect(result).toEqual({ fullName: 'John Doe' })
})

it('should handle transmutation with both "from" and "fn"', () => {
const schema: Schema<SourceUser, TargetUser>[] = [
},
{
to: 'userAge',
from: ({ source }) => source['age'] + 1
to: 'isAdult',
from: ({ source }) => source.age >= 18
}
]

const result = transmute(schema, sourceUser)
expect(result).toEqual({ userAge: 26 })
expect(result).toEqual({
fullName: 'John Doe',
isAdult: true
})
})

it('should handle extra data in transmutations', () => {
it('should handle transmutation with extra data', () => {
interface Extra {
'separator': string
separator: string
}

const schema: Schema<SourceUser, TargetUser, Extra>[] = [
const schema: Schema<SourceUser, Pick<TargetUser, 'location'>, Extra>[] = [
{
to: 'location',
from: ({ source, extra }) =>
Expand All @@ -83,7 +78,7 @@ describe('transmute', () => {
expect(result).toEqual({ location: 'New York, USA | ' })
})

it('should handle multiple transmutations', () => {
it('should handle complete object transmutation', () => {
const schema: Schema<SourceUser, TargetUser>[] = [
{
to: 'fullName',
Expand Down Expand Up @@ -117,36 +112,4 @@ describe('transmute', () => {
isAdult: true
})
})

it('should keep undefined values as undefined', () => {
const schema: Schema<SourceUser, { optionalField: string }>[] = [
{
to: 'optionalField',
from: 'nonexistentField' as keyof SourceUser
}
]

const result = transmute(schema, sourceUser)
expect(result).toEqual({ optionalField: undefined })
})

it('should handle empty schema', () => {
const schema: Schema<SourceUser, {}>[] = []
const result = transmute(schema, sourceUser)
expect(result).toEqual({})
})

it('should handle null source values', () => {
const sourceWithNull = {
...sourceUser,
email: null as unknown as string
}

const schema: Schema<typeof sourceWithNull, Pick<TargetUser, 'contactEmail'>>[] = [
{ from: 'email', to: 'contactEmail' }
]

const result = transmute(schema, sourceWithNull)
expect(result).toEqual({ contactEmail: null })
})
})
8 changes: 4 additions & 4 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,16 @@ export * from './types'
* Transmutes an object from the Source type into the Target type based on the provided schema
* @template Source - The source type being transmuted from
* @template Target - The target type being transmuted to
* @template TExtra - Type of additional data passed to mutation functions
* @template Extra - Type of additional data passed to mutation functions
* @param schema - Array of mutation rules
* @param source - Source object to transmute
* @param extra - Optional extra data to pass to mutation functions
* @returns Transmuted object matching Target type
*/
export const transmute = <Source, Target, TExtra = unknown>(
schema: Schema<Source, Target, TExtra>[],
export const transmute = <Source, Target, Extra = unknown>(
schema: Schema<Source, Target, Extra>[],
source: Source,
extra?: TExtra
extra?: Extra
): Target =>
schema.reduce<Target>(
(acc, { from, to }) => ({
Expand Down
25 changes: 16 additions & 9 deletions src/types.ts
Original file line number Diff line number Diff line change
@@ -1,36 +1,43 @@
/**
* Arguments passed to a mutation function
* @template Source - The source type being transmuted from
* @template TExtra - Type of additional data for transmutation
* @template Extra - Type of additional data for transmutation
*/
export type TransmuteFnArgs<Source, TExtra> = {
export type TransmuteFnArgs<Source, Extra> = {
/** The source object being transmuted */
source: Source
/** Optional extra data to assist with transmutation */
extra?: TExtra
extra?: Extra
}

/**
* Function that performs a custom transmutation on a source object
* @template Source - The source type being transmuted from
* @template Target - The target type being transmuted to
* @template TargetKey - The specific key of the target property being set
* @template TExtra - Type of additional data for transmutation
* @template Extra - Type of additional data for transmutation
*/
export type TransmuteFn<Source, Target, TargetKey extends keyof Target, TExtra = unknown> =
(args: TransmuteFnArgs<Source, TExtra>) => Target[TargetKey]
export type TransmuteFn<Source, Target, TargetKey extends keyof Target, Extra> =
(args: TransmuteFnArgs<Source, Extra>) => Target[TargetKey]

/**
* Get keys of Source that have values assignable to Target[TargetKey]
*/
type AssignableKeys<Source, Target, TargetKey extends keyof Target> = {
[SourceKey in keyof Source]: Source[SourceKey] extends Target[TargetKey] ? SourceKey : never
}[keyof Source]

/**
* Defines how a property should be transmuted from source to target type
* @template Source - The source type being transmuted from
* @template Target - The target type being transmuted to
* @template TExtra - Type of additional data for transmutation
* @template Extra - Type of additional data for transmutation
*/
export type Schema<Source, Target, TExtra = unknown> = {
export type Schema<Source, Target, Extra = unknown> = {
[TargetKey in keyof Target]: {
/** Target property key */
to: TargetKey
/** Source property key for direct mapping or a custom transmutation function */
from: keyof Source | TransmuteFn<Source, Target, TargetKey, TExtra>
from: AssignableKeys<Source, Target, TargetKey> | TransmuteFn<Source, Target, TargetKey, Extra>
}
}[keyof Target]

0 comments on commit cc26b94

Please sign in to comment.