Skip to content

Commit 9cc11c4

Browse files
authored
Merge pull request #156 from nobrainr/fix/type-action-selector
Fix/type action selector
2 parents 438a168 + 1384bd9 commit 9cc11c4

File tree

2 files changed

+152
-65
lines changed

2 files changed

+152
-65
lines changed

src/types.ts

+50-12
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { SCHEMA_OPTIONS_SYMBOL, SchemaOptions } from './morphism';
1+
import { SCHEMA_OPTIONS_SYMBOL, SchemaOptions } from "./morphism";
22

33
/**
44
* A structure-preserving object from a source data towards a target data.
@@ -35,22 +35,38 @@ export type StrictSchema<Target = any, Source = any> = {
3535
/** `destinationProperty` is the name of the property of the target object you want to produce */
3636
[destinationProperty in keyof Target]:
3737
| ActionString<Source>
38-
| { (iteratee: Source, source: Source[], target: Target[destinationProperty]): Target[destinationProperty] }
38+
| {
39+
(
40+
iteratee: Source,
41+
source: Source[],
42+
target: Target[destinationProperty]
43+
): Target[destinationProperty];
44+
}
3945
| ActionAggregator<Source>
40-
| ActionSelector<Source, Target>
46+
| ActionSelector<Source, Target, destinationProperty>
4147
| StrictSchema<Target[destinationProperty], Source>;
4248
} & { [SCHEMA_OPTIONS_SYMBOL]?: SchemaOptions<Target> };
4349
export type Schema<Target = any, Source = any> = {
4450
/** `destinationProperty` is the name of the property of the target object you want to produce */
4551
[destinationProperty in keyof Target]?:
4652
| ActionString<Source>
47-
| { (iteratee: Source, source: Source[], target: Target[destinationProperty]): Target[destinationProperty] }
53+
| {
54+
(
55+
iteratee: Source,
56+
source: Source[],
57+
target: Target[destinationProperty]
58+
): Target[destinationProperty];
59+
}
4860
| ActionAggregator<Source>
49-
| ActionSelector<Source, Target>
61+
| ActionSelector<Source, Target, destinationProperty>
5062
| Schema<Target[destinationProperty], Source>;
5163
} & { [SCHEMA_OPTIONS_SYMBOL]?: SchemaOptions<Target | any> };
5264

53-
export type Actions<Target, Source> = ActionFunction<Target, Source> | ActionAggregator | ActionString<Target> | ActionSelector<Source>;
65+
export type Actions<Target, Source> =
66+
| ActionFunction<Target, Source>
67+
| ActionAggregator
68+
| ActionString<Target>
69+
| ActionSelector<Source>;
5470

5571
/**
5672
* @interface ActionFunction
@@ -129,7 +145,9 @@ export type ActionString<Source> = string; // TODO: ActionString should support
129145
* //=> { fooAndBar: { foo: 'foo', bar: 'bar' } }
130146
* ```
131147
*/
132-
export type ActionAggregator<T extends unknown = unknown> = T extends object ? (keyof T)[] | string[] : string[];
148+
export type ActionAggregator<T extends unknown = unknown> = T extends object
149+
? (keyof T)[] | string[]
150+
: string[];
133151
/**
134152
* @interface ActionSelector
135153
* @typeparam Source Source/Input Type
@@ -157,21 +175,41 @@ export type ActionAggregator<T extends unknown = unknown> = T extends object ? (
157175
*```
158176
*
159177
*/
160-
export interface ActionSelector<Source = object, R = any> {
178+
export interface ActionSelector<
179+
Source = object,
180+
Target = any,
181+
TargetProperty extends keyof Target = any
182+
> {
161183
path: ActionString<Source> | ActionAggregator<Source>;
162-
fn: (fieldValue: any, object: Source, items: Source, objectToCompute: R) => R;
184+
fn: (
185+
fieldValue: any,
186+
object: Source,
187+
items: Source,
188+
objectToCompute: Target
189+
) => Target[TargetProperty];
163190
}
164191

165192
export interface Constructable<T> {
166193
new (...args: any[]): T;
167194
}
168195

169-
export type SourceFromSchema<T> = T extends StrictSchema<unknown, infer U> | Schema<unknown, infer U> ? U : never;
170-
export type DestinationFromSchema<T> = T extends StrictSchema<infer U> | Schema<infer U> ? U : never;
196+
export type SourceFromSchema<T> = T extends
197+
| StrictSchema<unknown, infer U>
198+
| Schema<unknown, infer U>
199+
? U
200+
: never;
201+
export type DestinationFromSchema<T> = T extends
202+
| StrictSchema<infer U>
203+
| Schema<infer U>
204+
? U
205+
: never;
171206

172207
export type ResultItem<TSchema extends Schema> = DestinationFromSchema<TSchema>;
173208

174-
export interface Mapper<TSchema extends Schema | StrictSchema, TResult = ResultItem<TSchema>> {
209+
export interface Mapper<
210+
TSchema extends Schema | StrictSchema,
211+
TResult = ResultItem<TSchema>
212+
> {
175213
(data?: SourceFromSchema<TSchema>[] | null): TResult[];
176214
(data?: SourceFromSchema<TSchema> | null): TResult;
177215
}

src/typescript.spec.ts

+102-53
Original file line numberDiff line numberDiff line change
@@ -1,47 +1,52 @@
1-
import Morphism, { morphism, StrictSchema, Schema, createSchema } from './morphism';
2-
3-
describe('Typescript', () => {
4-
describe('Registry Type Checking', () => {
5-
it('Should return a Mapper when using Register', () => {
1+
import Morphism, {
2+
morphism,
3+
StrictSchema,
4+
Schema,
5+
createSchema
6+
} from "./morphism";
7+
8+
describe("Typescript", () => {
9+
describe("Registry Type Checking", () => {
10+
it("Should return a Mapper when using Register", () => {
611
class Foo {
712
foo: string;
813
}
9-
const schema = { foo: 'bar' };
10-
const source = { bar: 'value' };
14+
const schema = { foo: "bar" };
15+
const source = { bar: "value" };
1116
const mapper = Morphism.register(Foo, schema);
1217

13-
expect(mapper(source).foo).toEqual('value');
14-
expect(mapper([source][0]).foo).toEqual('value');
18+
expect(mapper(source).foo).toEqual("value");
19+
expect(mapper([source][0]).foo).toEqual("value");
1520
});
1621
});
1722

18-
describe('Schema Type Checking', () => {
19-
it('Should allow to type the Schema', () => {
23+
describe("Schema Type Checking", () => {
24+
it("Should allow to type the Schema", () => {
2025
interface IFoo {
2126
foo: string;
2227
bar: number;
2328
}
24-
const schema: Schema<IFoo> = { foo: 'qux' };
25-
const source = { qux: 'foo' };
29+
const schema: Schema<IFoo> = { foo: "qux" };
30+
const source = { qux: "foo" };
2631
const target = morphism(schema, source);
2732

2833
expect(target.foo).toEqual(source.qux);
2934
});
3035

31-
it('Should allow to use a strict Schema', () => {
36+
it("Should allow to use a strict Schema", () => {
3237
interface IFoo {
3338
foo: string;
3439
bar: number;
3540
}
36-
const schema: StrictSchema<IFoo> = { foo: 'qux', bar: () => 1 };
37-
const source = { qux: 'foo' };
41+
const schema: StrictSchema<IFoo> = { foo: "qux", bar: () => 1 };
42+
const source = { qux: "foo" };
3843
const target = morphism(schema, source);
3944

4045
expect(target.foo).toEqual(source.qux);
4146
expect(target.bar).toEqual(1);
4247
});
4348

44-
it('should accept 2 generic parameters on StrictSchema', () => {
49+
it("should accept 2 generic parameters on StrictSchema", () => {
4550
interface Source {
4651
inputA: string;
4752
inputB: string;
@@ -53,59 +58,61 @@ describe('Typescript', () => {
5358
fooC: string;
5459
}
5560
const schema: StrictSchema<Destination, Source> = {
56-
fooA: 'inputA',
61+
fooA: "inputA",
5762
fooB: ({ inputB }) => inputB,
58-
fooC: 'inputC'
63+
fooC: "inputC"
5964
};
6065

6166
const mapper = morphism(schema);
6267

63-
expect(mapper({ inputA: 'test', inputB: 'test2', inputC: 'test3' })).toEqual({
64-
fooA: 'test',
65-
fooB: 'test2',
66-
fooC: 'test3'
68+
expect(
69+
mapper({ inputA: "test", inputB: "test2", inputC: "test3" })
70+
).toEqual({
71+
fooA: "test",
72+
fooB: "test2",
73+
fooC: "test3"
6774
});
6875
});
6976

70-
it('should accept 2 generic parameters on Schema', () => {
77+
it("should accept 2 generic parameters on Schema", () => {
7178
interface Source2 {
7279
inputA: string;
7380
}
7481
const schema: Schema<{ foo: string }, Source2> = {
75-
foo: 'inputA'
82+
foo: "inputA"
7683
};
77-
morphism(schema, { inputA: 'test' });
78-
morphism(schema, [{ inputA: '' }]);
84+
morphism(schema, { inputA: "test" });
85+
morphism(schema, [{ inputA: "" }]);
7986
});
8087

81-
it('should accept 2 generic parameters on Schema', () => {
88+
it("should accept 2 generic parameters on Schema", () => {
8289
interface S {
8390
s1: string;
8491
}
8592
interface D {
8693
d1: string;
8794
}
8895
const schema: StrictSchema<D, S> = {
89-
d1: 's1'
96+
d1: "s1"
9097
};
91-
const a = morphism(schema)([{ s1: 'test' }]);
98+
const a = morphism(schema)([{ s1: "test" }]);
9299
const itemA = a.shift();
93100
expect(itemA).toBeDefined();
94101
if (itemA) {
95102
itemA.d1;
96103
}
97-
morphism(schema, { s1: 'teest' }).d1.toString();
98-
const b = morphism(schema, [{ s1: 'teest' }]);
104+
morphism(schema, { s1: "teest" }).d1.toString();
105+
const b = morphism(schema, [{ s1: "teest" }]);
99106
const itemB = b.shift();
100107
expect(itemB).toBeDefined();
101108
if (itemB) {
102109
itemB.d1;
103110
}
104-
morphism(schema, [{ s1: 'teest' }]);
105-
morphism(schema, [{ s1: 'test' }]);
111+
morphism(schema, [{ s1: "teest" }]);
112+
morphism(schema, [{ s1: "test" }]);
106113
});
107114

108-
it('should not fail with typescript', () => {
115+
it("should not fail with typescript", () => {
109116
interface S {
110117
s1: string;
111118
}
@@ -122,29 +129,45 @@ describe('Typescript', () => {
122129
namingIsHard: string;
123130
}
124131

125-
const a = morphism<StrictSchema<Destination, Source>>({ namingIsHard: 'boring_api_field' }, [{ boring_api_field: 2 }]);
132+
const a = morphism<StrictSchema<Destination, Source>>(
133+
{ namingIsHard: "boring_api_field" },
134+
[{ boring_api_field: 2 }]
135+
);
126136
const itemA = a.pop();
127137
expect(itemA).toBeDefined();
128138
if (itemA) {
129139
itemA.namingIsHard;
130140
}
131141

132-
const b = morphism<StrictSchema<Destination, Source>>({ namingIsHard: 'boring_api_field' }, { boring_api_field: 2 });
142+
const b = morphism<StrictSchema<Destination, Source>>(
143+
{ namingIsHard: "boring_api_field" },
144+
{ boring_api_field: 2 }
145+
);
133146
b.namingIsHard;
134147

135-
const c = morphism<StrictSchema<Destination>>({ namingIsHard: 'boring_api_field' }, [{ boring_api_field: 2 }]);
148+
const c = morphism<StrictSchema<Destination>>(
149+
{ namingIsHard: "boring_api_field" },
150+
[{ boring_api_field: 2 }]
151+
);
136152
const itemC = c.pop();
137153
expect(itemC).toBeDefined();
138154
if (itemC) {
139155
itemC.namingIsHard;
140156
}
141157

142-
const d = morphism<Destination>({ namingIsHard: 'boring_api_field' }, { boring_api_field: 2 });
158+
const d = morphism<Destination>(
159+
{ namingIsHard: "boring_api_field" },
160+
{ boring_api_field: 2 }
161+
);
143162
d.namingIsHard;
144163

145-
morphism({ namingIsHard: 'boring_api_field' });
146-
morphism<StrictSchema<Destination, Source>>({ namingIsHard: 'boring_api_field' })({ boring_api_field: 2 });
147-
const e = morphism<StrictSchema<Destination>>({ namingIsHard: 'boring_api_field' })([{ boring_api_field: 2 }]);
164+
morphism({ namingIsHard: "boring_api_field" });
165+
morphism<StrictSchema<Destination, Source>>({
166+
namingIsHard: "boring_api_field"
167+
})({ boring_api_field: 2 });
168+
const e = morphism<StrictSchema<Destination>>({
169+
namingIsHard: "boring_api_field"
170+
})([{ boring_api_field: 2 }]);
148171
const itemE = e.pop();
149172
expect(itemE).toBeDefined();
150173
if (itemE) {
@@ -162,7 +185,7 @@ describe('Typescript', () => {
162185
morphism<StrictSchema<D1, S1>>({ a: ({ _a }) => _a.toString() });
163186
});
164187

165-
it('shoud infer result type from source when a class is provided', () => {
188+
it("shoud infer result type from source when a class is provided", () => {
166189
class Source {
167190
constructor(public id: number, public ugly_field: string) {}
168191
}
@@ -171,31 +194,34 @@ describe('Typescript', () => {
171194
constructor(public id: number, public field: string) {}
172195
}
173196

174-
const source = [new Source(1, 'abc'), new Source(1, 'def')];
197+
const source = [new Source(1, "abc"), new Source(1, "def")];
175198

176199
const schema: StrictSchema<Destination, Source> = {
177-
id: 'id',
178-
field: 'ugly_field'
200+
id: "id",
201+
field: "ugly_field"
179202
};
180-
const expected = [new Destination(1, 'abc'), new Destination(1, 'def')];
203+
const expected = [new Destination(1, "abc"), new Destination(1, "def")];
181204

182205
const result = morphism(schema, source, Destination);
183206
result.forEach((item, idx) => {
184207
expect(item).toEqual(expected[idx]);
185208
});
186209
});
187210

188-
it('should accept union types as Target', () => {
189-
const schema = createSchema<{ a: string } | { a: string; b: string }, { c: string }>({
211+
it("should accept union types as Target", () => {
212+
const schema = createSchema<
213+
{ a: string } | { a: string; b: string },
214+
{ c: string }
215+
>({
190216
a: ({ c }) => c
191217
});
192218

193-
expect(morphism(schema, { c: 'result' }).a).toEqual('result');
219+
expect(morphism(schema, { c: "result" }).a).toEqual("result");
194220
});
195221
});
196222

197-
describe('Morphism Function Type Checking', () => {
198-
it('should infer target type from array input', () => {
223+
describe("Morphism Function Type Checking", () => {
224+
it("should infer target type from array input", () => {
199225
interface Source {
200226
ID: number;
201227
}
@@ -206,9 +232,32 @@ describe('Typescript', () => {
206232

207233
const rows: Array<Source> = [{ ID: 1234 }];
208234

209-
const schema: StrictSchema<Destination, Source> = { id: 'ID' };
235+
const schema: StrictSchema<Destination, Source> = { id: "ID" };
210236
expect(morphism(schema, rows)).toBeDefined();
211237
expect(morphism(schema, rows)[0].id).toEqual(1234);
212238
});
213239
});
240+
241+
describe("Selector Action", () => {
242+
it("should match return type of fn with target property", () => {
243+
interface Source {
244+
foo: string;
245+
}
246+
247+
interface Target {
248+
foo: number;
249+
}
250+
251+
const schema: StrictSchema<Target, Source> = {
252+
foo: {
253+
path: "foo",
254+
fn: val => {
255+
return Number(val);
256+
}
257+
}
258+
};
259+
const source: Source = { foo: "1" };
260+
expect(morphism(schema, source)).toEqual({ foo: 1 });
261+
});
262+
});
214263
});

0 commit comments

Comments
 (0)