Skip to content

Commit edbefa3

Browse files
authored
Merge pull request #40 from nobrainr/feat/support-inferred-types
Feat/support inferred types
2 parents 54f7099 + c29848f commit edbefa3

File tree

4 files changed

+129
-57
lines changed

4 files changed

+129
-57
lines changed

src/morphism.spec.ts

+35-6
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import Morphism from './morphism';
1+
import Morphism, { StrictSchema } from './morphism';
22

33
class User {
44
firstName: string;
@@ -29,7 +29,28 @@ class User {
2929
}
3030

3131
describe('Morphism', () => {
32-
const dataToCrunch = [
32+
interface MockData {
33+
firstName: string;
34+
lastName: string;
35+
age: number;
36+
address: {
37+
streetAddress: string;
38+
city: string;
39+
state: string;
40+
postalCode: string;
41+
};
42+
phoneNumber: [
43+
{
44+
type: string;
45+
number: string;
46+
},
47+
{
48+
type: string;
49+
number: string;
50+
}
51+
];
52+
}
53+
const dataToCrunch: MockData[] = [
3354
{
3455
firstName: 'John',
3556
lastName: 'Smith',
@@ -200,7 +221,7 @@ describe('Morphism', () => {
200221
describe('Function Predicate', function() {
201222
it('should support es6 destructuring as function predicate', function() {
202223
let schema = {
203-
target: ({ source }: any) => source
224+
target: ({ source }: { source: string }) => source
204225
};
205226
let mock = {
206227
source: 'value'
@@ -210,6 +231,7 @@ describe('Morphism', () => {
210231
};
211232
let result = Morphism(schema, mock);
212233
expect(result).toEqual(expected);
234+
expect(result.target).toEqual(expected.target);
213235
});
214236

215237
it('should support nesting mapping', function() {
@@ -348,11 +370,18 @@ describe('Morphism', () => {
348370
});
349371

350372
it('should pass the object value to the function when no path is specified', function() {
351-
let schema = {
373+
interface D {
374+
firstName: string;
375+
lastName: string;
376+
city: string;
377+
status: string;
378+
}
379+
380+
let schema: StrictSchema<D, MockData> = {
352381
firstName: 'firstName',
353382
lastName: 'lastName',
354-
city: 'address.city',
355-
status: (o: any) => o.phoneNumber[0].type
383+
city: { path: 'address.city', fn: prop => prop },
384+
status: o => o.phoneNumber[0].type
356385
};
357386

358387
let desiredResult = {

src/morphism.ts

+50-40
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,6 @@
33
*/
44
import { isString, get, isFunction, zipObject, isUndefined, assignInWith, aggregator, isObject } from './helpers';
55

6-
export function isActionSelector(value: any): value is ActionSelector {
7-
return isObject(value);
8-
}
96
/**
107
* A Function invoked per iteration
118
* @param {} iteratee The current element to transform
@@ -31,8 +28,8 @@ export function isActionSelector(value: any): value is ActionSelector {
3128
* ```
3229
*
3330
*/
34-
export interface ActionFunction<D, S> {
35-
(iteratee: S, source: S[], target: D): any;
31+
export interface ActionFunction<D, S, R> {
32+
(iteratee: S, source: S[], target: D): R;
3633
}
3734
/**
3835
* A String path that indicates where to find the property in the source input
@@ -100,7 +97,7 @@ export type ActionAggregator = string[];
10097
*```
10198
*
10299
*/
103-
export type ActionSelector = { path: string | string[]; fn: (fieldValue: any, items: any[]) => any };
100+
export type ActionSelector<Source, R> = { path: string | string[]; fn: (fieldValue: any, items: Source[]) => R };
104101

105102
/**
106103
* A structure-preserving object from a source data towards a target data.
@@ -134,23 +131,25 @@ export type ActionSelector = { path: string | string[]; fn: (fieldValue: any, it
134131
* ```
135132
*/
136133

137-
export type StrictSchema<Target = {}, Source = any> = {
134+
export type StrictSchema<Target = any, Source = any> = {
138135
/** `destinationProperty` is the name of the property of the target object you want to produce */
139136
[destinationProperty in keyof Target]:
140137
| ActionString<Source>
141-
| ActionFunction<Target, Source>
138+
| ActionFunction<Target, Source, Target[destinationProperty]>
142139
| ActionAggregator
143-
| ActionSelector
140+
| ActionSelector<Source, Target[destinationProperty]>
144141
};
145-
export type Schema<Target = {}, Source = any> = {
142+
export type Schema<Target = any, Source = any> = {
146143
/** `destinationProperty` is the name of the property of the target object you want to produce */
147144
[destinationProperty in keyof Target]?:
148145
| ActionString<Source>
149-
| ActionFunction<Target, Source>
146+
| ActionFunction<Target, Source, Target[destinationProperty]>
150147
| ActionAggregator
151-
| ActionSelector
148+
| ActionSelector<Source, Target[destinationProperty]>
152149
};
153-
150+
export function isActionSelector<S, R>(value: any): value is ActionSelector<S, R> {
151+
return isObject(value);
152+
}
154153
/**
155154
* Low Level transformer function.
156155
* Take a plain object as input and transform its values using a specified schema.
@@ -218,11 +217,6 @@ interface Constructable<T> {
218217
new (...args: any[]): T;
219218
}
220219

221-
export interface Mapper<Target> {
222-
<Source>(source: Source[]): Target[];
223-
<Source>(source: Source): Target;
224-
}
225-
226220
function transformItems<T, TSchema extends Schema<T>>(schema: TSchema): Mapper<{ [P in keyof TSchema]: any }>;
227221
function transformItems<T, TSchema extends Schema<T>>(
228222
schema: TSchema,
@@ -266,6 +260,15 @@ function getSchemaForType<T>(type: Constructable<T>, baseSchema: Schema<T>): Sch
266260
return finalSchema;
267261
}
268262

263+
type SourceFromSchema<T> = T extends StrictSchema<unknown, infer U> | Schema<unknown, infer U> ? U : never;
264+
type DestinationFromSchema<T> = T extends StrictSchema<infer U> | Schema<infer U> ? U : never;
265+
266+
type ResultItem<TSchema extends Schema> = { [P in keyof TSchema]: DestinationFromSchema<TSchema>[P] };
267+
export interface Mapper<TSchema extends Schema | StrictSchema, TResult = ResultItem<TSchema>> {
268+
(data: Partial<SourceFromSchema<TSchema>>[]): TResult[];
269+
(data: Partial<SourceFromSchema<TSchema>>): TResult;
270+
}
271+
269272
/**
270273
* Currying function that either outputs a mapping function or the transformed data.
271274
*
@@ -288,28 +291,35 @@ function getSchemaForType<T>(type: Constructable<T>, baseSchema: Schema<T>): Sch
288291
* @param {} type
289292
*
290293
*/
291-
export function morphism<TSchema extends Schema<{}, Source>, Source>(
294+
export function morphism<TSchema extends Schema, Source extends SourceFromSchema<TSchema>>(
292295
schema: TSchema,
293-
items: Source
294-
): Source extends any[] ? { [P in keyof TSchema]: any }[] : { [P in keyof TSchema]: any };
295-
// morphism({},{}) => {}
296-
// morphism({},[]) => Target[]
296+
data: Source
297+
): Source extends any[] ? ResultItem<TSchema>[] : ResultItem<TSchema>;
297298

298-
export function morphism<TSchema extends Schema<any>>(schema: TSchema): Mapper<{ [P in keyof TSchema]: any }>; // morphism(TSchema) => mapper(S[]) => (keyof TSchema)[]
299-
export function morphism<Target>(schema: Schema<Target>): Mapper<Target>; // morphism<ITarget>({}) => Mapper<ITarget> => ITarget
300-
export function morphism<Target, Source>(
301-
schema: Schema<Target>,
299+
export function morphism<TSchema extends Schema, Source extends SourceFromSchema<TSchema>>(
300+
schema: TSchema,
301+
data: Source[]
302+
): ResultItem<TSchema>[];
303+
304+
export function morphism<TSchema extends Schema>(schema: TSchema): Mapper<TSchema>; // morphism({}) => mapper(S) => T
305+
306+
export function morphism<TSchema extends Schema, TDestination>(
307+
schema: TSchema,
302308
items: null,
309+
type: Constructable<TDestination>
310+
): Mapper<TSchema, TDestination>; // morphism({}, null, T) => mapper(S) => T
311+
312+
export function morphism<TSchema extends Schema, Target>(
313+
schema: TSchema,
314+
items: SourceFromSchema<TSchema>,
303315
type: Constructable<Target>
304-
): Mapper<Target>; // morphism({}, null, T) => mapper(S) => T
305-
export function morphism<Target, Source>(
306-
schema: Schema<Target>,
307-
items: Source[],
308-
type: Constructable<Target>
309-
): Target[]; // morphism({}, [], T) => T[]
310-
export function morphism<Target, Source>(schema: Schema<Target>, items: Source, type: Constructable<Target>): Target; // morphism({}, {}, T) => T
316+
): Target; // morphism({}, {}, T) => T
311317

312-
export function morphism<Target, Source>(schema: Schema<Target>, items?: Source, type?: Constructable<Target>) {
318+
export function morphism<Target, Source, TSchema extends Schema<Target, Source>>(
319+
schema: TSchema,
320+
items?: SourceFromSchema<TSchema>,
321+
type?: Constructable<Target>
322+
): any {
313323
if (items === undefined && type === undefined) {
314324
return transformItems(schema);
315325
} else if (schema && items && type) {
@@ -336,15 +346,15 @@ export interface IMorphismRegistry {
336346
* @param schema Structure-preserving object from a source data towards a target data.
337347
*
338348
*/
339-
register<Target, TSchema extends Schema<Target>>(type: Constructable<Target>, schema?: TSchema): Mapper<Target>;
349+
register<Target, TSchema>(type: Constructable<Target>, schema?: TSchema): Mapper<TSchema, Target>;
340350
/**
341351
* Transform any input in the specified Class
342352
*
343353
* @param {Type} type Class Type of the ouput Data
344354
* @param {Object} data Input data to transform
345355
*
346356
*/
347-
map<Target>(type: Target): Mapper<Target>;
357+
map<Target>(type: Target): Mapper<Schema, Target>;
348358
map<Target, Source>(type: Constructable<Target>, data: Source[]): Target[];
349359
map<Target, Source>(type: Constructable<Target>, data: Source): Target;
350360
/**
@@ -353,15 +363,15 @@ export interface IMorphismRegistry {
353363
* @param {Type} type Class Type of the ouput Data
354364
*
355365
*/
356-
getMapper<Target>(type: Constructable<Target>): Mapper<Target>;
366+
getMapper<Target>(type: Constructable<Target>): Mapper<Schema, Target>;
357367
/**
358368
* Set a schema for a specific Class Type
359369
*
360370
* @param {Type} type Class Type of the ouput Data
361371
* @param {Schema} schema Class Type of the ouput Data
362372
*
363373
*/
364-
setMapper<Target, TSchema extends Schema<Target>>(type: Constructable<Target>, schema: TSchema): Mapper<Target>;
374+
setMapper<Target, TSchema extends Schema<Target>>(type: Constructable<Target>, schema: TSchema): Mapper<any, Target>;
365375
/**
366376
* Delete a registered schema associated to a Class
367377
*
@@ -405,7 +415,7 @@ export class MorphismRegistry implements IMorphismRegistry {
405415
* @param schema Structure-preserving object from a source data towards a target data.
406416
*
407417
*/
408-
register<Target, TSchema extends Schema<Target>>(type: Constructable<Target>, schema?: TSchema) {
418+
register<Target, TSchema>(type: Constructable<Target>, schema?: TSchema) {
409419
if (!type && !schema) {
410420
throw new Error('type paramater is required when register a mapping');
411421
} else if (this.exists(type)) {

src/typescript.spec.ts

+39-4
Original file line numberDiff line numberDiff line change
@@ -13,14 +13,19 @@ describe('Morphism', () => {
1313
fooB: string;
1414
fooC: string;
1515
}
16-
const schema = {
16+
const schema: StrictSchema<Destination, Source> = {
1717
fooA: 'inputA',
1818
fooB: ({ inputB }) => inputB,
1919
fooC: 'inputC'
20-
} as StrictSchema<Destination, Source>;
20+
};
2121

2222
const mapper = morphism(schema);
23-
const res = mapper({}) as Destination;
23+
24+
expect(mapper({ inputA: 'test', inputB: 'test2', inputC: 'test3' })).toEqual({
25+
fooA: 'test',
26+
fooB: 'test2',
27+
fooC: 'test3'
28+
});
2429
});
2530

2631
it('should accept 2 generic parameters on Schema', () => {
@@ -31,8 +36,38 @@ describe('Morphism', () => {
3136
foo: 'inputA'
3237
};
3338
morphism(schema, { inputA: 'test' });
34-
3539
morphism(schema, [{ inputA: '' }]);
3640
});
41+
42+
it('should accept 2 generic parameters on Schema', () => {
43+
interface S {
44+
s1: string;
45+
}
46+
interface D {
47+
d1: string;
48+
}
49+
const schema: StrictSchema<D, S> = {
50+
d1: 's1'
51+
};
52+
morphism(schema)([{ s1: 'test' }]).shift().d1;
53+
morphism(schema, { s1: 'teest' }).d1.toString();
54+
morphism(schema, [{ s1: 'teest' }]).shift().d1;
55+
morphism(schema, [{ s1: 'teest' }]);
56+
morphism(schema, [{ s1: 'test' }]);
57+
// morphism(schema,[{}])
58+
});
59+
60+
// xit('should fail with typescript', () => {
61+
// interface S {
62+
// s1: string;
63+
// }
64+
// interface D {
65+
// d1: string;
66+
// }
67+
// const schema: StrictSchema<D, S> = {
68+
// d1: 's1'
69+
// };
70+
// morphism(schema, [{ toto: 'test' }]);
71+
// });
3772
});
3873
});

src/typings.spec.ts

+5-7
Original file line numberDiff line numberDiff line change
@@ -30,9 +30,9 @@ describe('Morphism', () => {
3030
interface IFoo {
3131
foo: string;
3232
}
33-
const schema = { foo: 'bar' };
33+
const schema: Schema<IFoo> = { foo: 'bar' };
3434
const source = { bar: 'value' };
35-
const mapper = morphism<IFoo>(schema);
35+
const mapper = morphism(schema);
3636

3737
expect(mapper(source).foo).toEqual('value');
3838
expect(mapper([source][0]).foo).toEqual('value');
@@ -50,9 +50,7 @@ describe('Morphism', () => {
5050
const schema: StrictSchema<Destination, Source> = {
5151
foo: 'bar',
5252
bar: 'bar',
53-
qux: elem => {
54-
elem.bar;
55-
}
53+
qux: elem => elem.bar
5654
};
5755
const source = { bar: 'value' };
5856
// const target2 = morphism(
@@ -114,12 +112,12 @@ describe('Morphism', () => {
114112
foo: string;
115113
bar: number;
116114
}
117-
const schema: StrictSchema<IFoo> = { foo: 'qux', bar: () => 'test' };
115+
const schema: StrictSchema<IFoo> = { foo: 'qux', bar: () => 1 };
118116
const source = { qux: 'foo' };
119117
const target = morphism(schema, source);
120118

121119
expect(target.foo).toEqual(source.qux);
122-
expect(target.bar).toEqual('test');
120+
expect(target.bar).toEqual(1);
123121
});
124122
});
125123
});

0 commit comments

Comments
 (0)