Skip to content

Commit 29ef5e9

Browse files
author
Janis Köhr
committed
Added the possibility to provide an array of discriminator functions to convert into different types in case of a single property and for nested objects in arrays
1 parent 9854ed7 commit 29ef5e9

File tree

5 files changed

+418
-54
lines changed

5 files changed

+418
-54
lines changed

README.md

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -267,6 +267,118 @@ let album = plainToClass(Album, albumJson);
267267
// now album is Album object with Photo objects inside
268268
```
269269

270+
### Providing more than one type option
271+
272+
In case the nested object can be of different types, you can provide an array of
273+
discriminator functions instead of a single type definition. These functions are then used
274+
to determine the correct target type for the nested object.
275+
276+
Lets say we have an album that has a top photo. But this photo can be of certain different types.
277+
And we are trying to convert album plain object to class object:
278+
279+
```typescript
280+
import {Type, plainToClass} from "class-transformer";
281+
282+
export abstract class Photo {
283+
id: number;
284+
filename: string;
285+
}
286+
287+
export class Landscape extends Photo {
288+
panorama: boolean;
289+
}
290+
291+
export class Portrait extends Photo {
292+
person: Person;
293+
}
294+
295+
export class UnderWater extends Photo {
296+
depth: number;
297+
}
298+
299+
function isLandscape(value: any): Function | false {
300+
return value.panorama !== undefined ? Landscape : false;
301+
}
302+
303+
function isPortrait(value: any): Function | false {
304+
return value.person !== undefined ? Portrait : false;
305+
}
306+
307+
function isUnderWater(value: any): Function | false {
308+
return value.depth !== undefined ? UnderWater : false;
309+
}
310+
311+
export class Album {
312+
313+
id: number;
314+
name: string;
315+
316+
@Type(() => [ isLandscape, isPortrait, isUnderWater ])
317+
topPhoto: Landscape | Portrait | UnderWater;
318+
319+
}
320+
321+
let album = plainToClass(Album, albumJson);
322+
// now album is Album object with Landscape, Portrait and UnderWater objects inside
323+
```
324+
325+
### Working with an array that holds more than one nested object type
326+
327+
In case you have to deal with an array that holds different kinds of nested objects,
328+
the `@Type` decorator can be used, too. Here you just give an array of discriminator functions
329+
instead of a single type definition, in order to be able to determine which type each object has.
330+
331+
Lets stick to the photo album example from above, but here we got different photo types.
332+
And we are trying to convert the whole album plain object to its class.
333+
334+
```typescript
335+
import {Type, plainToClass} from "class-transformer";
336+
337+
export abstract class Photo {
338+
id: number;
339+
filename: string;
340+
}
341+
342+
export class Landscape extends Photo {
343+
panorama: boolean;
344+
}
345+
346+
export class Portrait extends Photo {
347+
person: Person;
348+
}
349+
350+
export class UnderWater extends Photo {
351+
depth: number;
352+
}
353+
354+
function isLandscape(value: any): Function | false {
355+
return value.panorama !== undefined ? Landscape : false;
356+
}
357+
358+
function isPortrait(value: any): Function | false {
359+
return value.person !== undefined ? Portrait : false;
360+
}
361+
362+
function isUnderWater(value: any): Function | false {
363+
return value.depth !== undefined ? UnderWater : false;
364+
}
365+
366+
export class Album {
367+
368+
id: number;
369+
name: string;
370+
371+
@Type(() => [ isLandscape, isPortrait, isUnderWater ])
372+
photos: (Landscape | Portrait | UnderWater)[];
373+
}
374+
375+
let album = plainToClass(Album, albumJson);
376+
// now album is Album object with Landscape, Portrait and UnderWater objects inside
377+
```
378+
379+
Hint: You can also provide a special discriminator function, which returns a standard type,
380+
as the last element of the type array such that it gets a type for sure.
381+
270382
## Exposing getters and method return values
271383

272384
You can expose what your getter or method return by setting a `@Expose()` decorator to those getters or methods:

src/TransformOperationExecutor.ts

Lines changed: 47 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1-
import {ClassTransformOptions} from "./ClassTransformOptions";
2-
import {defaultMetadataStorage} from "./storage";
3-
import {TypeOptions} from "./metadata/ExposeExcludeOptions";
1+
import { ClassTransformOptions } from "./ClassTransformOptions";
2+
import { defaultMetadataStorage } from "./storage";
3+
import { TypeOptions } from "./metadata/ExposeExcludeOptions";
4+
import { DiscrimnatorFunction } from "./metadata/TypeMetadata";
45

56
export enum TransformationType {
67
PLAIN_TO_CLASS,
@@ -14,33 +15,46 @@ export class TransformOperationExecutor {
1415
// Private Properties
1516
// -------------------------------------------------------------------------
1617

17-
private transformedTypesMap = new Map<Object, {level: number, object: Object}>();
18+
private transformedTypesMap = new Map<Object, { level: number, object: Object }>();
1819

1920
// -------------------------------------------------------------------------
2021
// Constructor
2122
// -------------------------------------------------------------------------
2223

2324
constructor(private transformationType: TransformationType,
24-
private options: ClassTransformOptions) {
25+
private options: ClassTransformOptions) {
2526
}
2627

2728
// -------------------------------------------------------------------------
2829
// Public Methods
2930
// -------------------------------------------------------------------------
3031

31-
transform(source: Object|Object[]|any,
32-
value: Object|Object[]|any,
33-
targetType: Function,
34-
arrayType: Function,
35-
isMap: boolean,
36-
level: number = 0) {
32+
transform(source: Object | Object[] | any,
33+
value: Object | Object[] | any,
34+
targetType: Function | DiscrimnatorFunction[],
35+
arrayType: Function,
36+
isMap: boolean,
37+
level: number = 0) {
3738

3839
if (value instanceof Array || value instanceof Set) {
3940
const newValue = arrayType && this.transformationType === TransformationType.PLAIN_TO_CLASS ? new (arrayType as any)() : [];
4041
(value as any[]).forEach((subValue, index) => {
4142
const subSource = source ? source[index] : undefined;
4243
if (!this.options.enableCircularCheck || !this.isCircular(subValue, level)) {
43-
const value = this.transform(subSource, subValue, targetType, undefined, subValue instanceof Map, level + 1);
44+
let realTargetType;
45+
if (Array.isArray(targetType)) {
46+
for (const func of targetType) {
47+
const potentialType = func(subValue);
48+
if (potentialType) {
49+
realTargetType = potentialType;
50+
break;
51+
}
52+
}
53+
if (typeof realTargetType !== "function") throw Error("None of the given discriminator functions did return a constructor for a type.");
54+
value = this.transform(subSource, subValue, realTargetType, undefined, subValue instanceof Map, level + 1);
55+
} else {
56+
value = this.transform(subSource, subValue, targetType, undefined, subValue instanceof Map, level + 1);
57+
}
4458
if (newValue instanceof Set) {
4559
newValue.add(value);
4660
} else {
@@ -55,7 +69,6 @@ export class TransformOperationExecutor {
5569
}
5670
});
5771
return newValue;
58-
5972
} else if (targetType === String && !isMap) {
6073
return String(value);
6174

@@ -76,16 +89,28 @@ export class TransformOperationExecutor {
7689

7790
} else if (value instanceof Object) {
7891

92+
if (Array.isArray(targetType)) {
93+
for (const func of targetType) {
94+
const potentialType = func(value);
95+
if (potentialType) {
96+
targetType = potentialType;
97+
break;
98+
}
99+
}
100+
if (typeof targetType !== "function") throw Error("None of the given discriminator functions did return a constructor for a type.");
101+
}
102+
79103
// try to guess the type
80104
if (!targetType && value.constructor !== Object/* && TransformationType === TransformationType.CLASS_TO_PLAIN*/) targetType = value.constructor;
81105
if (!targetType && source) targetType = source.constructor;
82106

107+
83108
if (this.options.enableCircularCheck) {
84109
// add transformed type to prevent circular references
85-
this.transformedTypesMap.set(value, {level: level, object: value});
110+
this.transformedTypesMap.set(value, { level: level, object: value });
86111
}
87112

88-
const keys = this.getKeys(targetType, value);
113+
const keys = this.getKeys((targetType as Function), value);
89114
let newValue: any = source ? source : {};
90115
if (!source && (this.transformationType === TransformationType.PLAIN_TO_CLASS || this.transformationType === TransformationType.CLASS_TO_CLASS)) {
91116
if (isMap) {
@@ -103,14 +128,14 @@ export class TransformOperationExecutor {
103128
let valueKey = key, newValueKey = key, propertyName = key;
104129
if (!this.options.ignoreDecorators && targetType) {
105130
if (this.transformationType === TransformationType.PLAIN_TO_CLASS) {
106-
const exposeMetadata = defaultMetadataStorage.findExposeMetadataByCustomName(targetType, key);
131+
const exposeMetadata = defaultMetadataStorage.findExposeMetadataByCustomName((targetType as Function), key);
107132
if (exposeMetadata) {
108133
propertyName = exposeMetadata.propertyName;
109134
newValueKey = exposeMetadata.propertyName;
110135
}
111136

112137
} else if (this.transformationType === TransformationType.CLASS_TO_PLAIN || this.transformationType === TransformationType.CLASS_TO_CLASS) {
113-
const exposeMetadata = defaultMetadataStorage.findExposeMetadata(targetType, key);
138+
const exposeMetadata = defaultMetadataStorage.findExposeMetadata((targetType as Function), key);
114139
if (exposeMetadata && exposeMetadata.options && exposeMetadata.options.name)
115140
newValueKey = exposeMetadata.options.name;
116141
}
@@ -132,9 +157,9 @@ export class TransformOperationExecutor {
132157
type = targetType;
133158

134159
} else if (targetType) {
135-
const metadata = defaultMetadataStorage.findTypeMetadata(targetType, propertyName);
160+
const metadata = defaultMetadataStorage.findTypeMetadata((targetType as Function), propertyName);
136161
if (metadata) {
137-
const options: TypeOptions = {newObject: newValue, object: value, property: propertyName};
162+
const options: TypeOptions = { newObject: newValue, object: value, property: propertyName };
138163
type = metadata.typeFunction(options);
139164
isSubValueMap = isSubValueMap || metadata.reflectedType === Map;
140165
} else if (this.options.targetMaps) { // try to find a type in target maps
@@ -145,7 +170,7 @@ export class TransformOperationExecutor {
145170
}
146171

147172
// if value is an array try to get its custom array type
148-
const arrayType = value[valueKey] instanceof Array ? this.getReflectedType(targetType, propertyName) : undefined;
173+
const arrayType = value[valueKey] instanceof Array ? this.getReflectedType((targetType as Function), propertyName) : undefined;
149174
// const subValueKey = TransformationType === TransformationType.PLAIN_TO_CLASS && newKeyName ? newKeyName : key;
150175
const subSource = source ? source[valueKey] : undefined;
151176

@@ -165,15 +190,15 @@ export class TransformOperationExecutor {
165190
if (!this.options.enableCircularCheck || !this.isCircular(subValue, level)) {
166191
let transformKey = this.transformationType === TransformationType.PLAIN_TO_CLASS ? newValueKey : key;
167192
let finalValue = this.transform(subSource, subValue, type, arrayType, isSubValueMap, level + 1);
168-
finalValue = this.applyCustomTransformations(finalValue, targetType, transformKey, value, this.transformationType);
193+
finalValue = this.applyCustomTransformations(finalValue, (targetType as Function), transformKey, value, this.transformationType);
169194
if (newValue instanceof Map) {
170195
newValue.set(newValueKey, finalValue);
171196
} else {
172197
newValue[newValueKey] = finalValue;
173198
}
174199
} else if (this.transformationType === TransformationType.CLASS_TO_CLASS) {
175200
let finalValue = subValue;
176-
finalValue = this.applyCustomTransformations(finalValue, targetType, key, value, this.transformationType);
201+
finalValue = this.applyCustomTransformations(finalValue, (targetType as Function), key, value, this.transformationType);
177202
if (newValue instanceof Map) {
178203
newValue.set(newValueKey, finalValue);
179204
} else {

src/decorators.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import {ClassTransformer} from "./ClassTransformer";
22
import {defaultMetadataStorage} from "./storage";
3-
import {TypeMetadata} from "./metadata/TypeMetadata";
3+
import {TypeMetadata, DiscrimnatorFunction} from "./metadata/TypeMetadata";
44
import {ExposeMetadata} from "./metadata/ExposeMetadata";
55
import {ExposeOptions, ExcludeOptions, TypeOptions, TransformOptions} from "./metadata/ExposeExcludeOptions";
66
import {ExcludeMetadata} from "./metadata/ExcludeMetadata";
@@ -20,8 +20,10 @@ export function Transform(transformFn: (value: any, obj: any, transformationType
2020

2121
/**
2222
* Specifies a type of the property.
23+
* The given TypeFunction can return a constructor or in case of an array of different types
24+
* an array of discriminator functions can be provided.
2325
*/
24-
export function Type(typeFunction?: (type?: TypeOptions) => Function) {
26+
export function Type(typeFunction?: (type?: TypeOptions) => Function | DiscrimnatorFunction[]) {
2527
return function(target: any, key: string) {
2628
const type = (Reflect as any).getMetadata("design:type", target, key);
2729
const metadata = new TypeMetadata(target.constructor, key, type, typeFunction);

src/metadata/TypeMetadata.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,9 @@ export class TypeMetadata {
55
constructor(public target: Function,
66
public propertyName: string,
77
public reflectedType: any,
8-
public typeFunction: (options?: TypeOptions) => Function) {
8+
public typeFunction: (options?: TypeOptions) => Function | DiscrimnatorFunction[]) {
99
}
1010

11-
}
11+
}
12+
13+
export type DiscrimnatorFunction = (value: any) => Function | false;

0 commit comments

Comments
 (0)