Skip to content

Commit a650d9f

Browse files
authored
Merge pull request #125 from janis91/polymorph
Polymorphism
2 parents c7dfa51 + a2aade0 commit a650d9f

File tree

6 files changed

+654
-57
lines changed

6 files changed

+654
-57
lines changed

README.md

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -267,6 +267,78 @@ 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 additional options object,
273+
that specifies a discriminator. The discriminator option must define a `property` that holds the sub
274+
type name for the object and the possible `subTypes`, the nested object can converted to. A sub type
275+
has a `value`, that holds the constructor of the Type and the `name`, that can match with the `property`
276+
of the discriminator.
277+
278+
Lets say we have an album that has a top photo. But this photo can be of certain different types.
279+
And we are trying to convert album plain object to class object. The plain object input has to define
280+
the additional property `__type`. This property is removed during transformation by default:
281+
282+
**JSON input**:
283+
```json
284+
{
285+
"id": 1,
286+
"name": "foo",
287+
"topPhoto": {
288+
"id": 9,
289+
"filename": "cool_wale.jpg",
290+
"depth": 1245,
291+
"__type": "underwater"
292+
}
293+
}
294+
```
295+
296+
```typescript
297+
import {Type, plainToClass} from "class-transformer";
298+
299+
export abstract class Photo {
300+
id: number;
301+
filename: string;
302+
}
303+
304+
export class Landscape extends Photo {
305+
panorama: boolean;
306+
}
307+
308+
export class Portrait extends Photo {
309+
person: Person;
310+
}
311+
312+
export class UnderWater extends Photo {
313+
depth: number;
314+
}
315+
316+
export class Album {
317+
318+
id: number;
319+
name: string;
320+
321+
@Type(() => Photo, {
322+
discriminator: {
323+
property: "__type",
324+
subTypes: [
325+
{ value: Landscape, name: "landscape" },
326+
{ value: Portrait, name: "portrait" },
327+
{ value: UnderWater, name: "underwater" }
328+
]
329+
}
330+
})
331+
topPhoto: Landscape | Portrait | UnderWater;
332+
333+
}
334+
335+
let album = plainToClass(Album, albumJson);
336+
// now album is Album object with a UnderWater object without `__type` property.
337+
```
338+
339+
Hint: The same applies for arrays with different sub types. Moreover you can specify `keepDiscriminatorProperty: true`
340+
in the options to keep the discriminator property also inside your resulting class.
341+
270342
## Exposing getters and method return values
271343

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

src/TransformOperationExecutor.ts

Lines changed: 64 additions & 23 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 { TypeHelpOptions, Discriminator, TypeOptions } from "./metadata/ExposeExcludeOptions";
4+
import { TypeMetadata } from "./metadata/TypeMetadata";
45

56
export enum TransformationType {
67
PLAIN_TO_CLASS,
@@ -21,26 +22,45 @@ export class TransformOperationExecutor {
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 | TypeMetadata,
35+
arrayType: Function,
36+
isMap: boolean,
37+
level: number = 0) {
3738

3839
if (Array.isArray(value) || 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)) {
43-
const value = this.transform(subSource, subValue, targetType, undefined, subValue instanceof Map, level + 1);
44+
let realTargetType;
45+
if (typeof targetType !== "function" && targetType && targetType.options && targetType.options.discriminator && targetType.options.discriminator.property && targetType.options.discriminator.subTypes) {
46+
if (this.transformationType === TransformationType.PLAIN_TO_CLASS) {
47+
realTargetType = targetType.options.discriminator.subTypes.find((subType) => subType.name === subValue[(targetType as { options: TypeOptions }).options.discriminator.property]);
48+
const options: TypeHelpOptions = { newObject: newValue, object: subValue, property: undefined };
49+
const newType = targetType.typeFunction(options);
50+
realTargetType === undefined ? realTargetType = newType : realTargetType = realTargetType.value;
51+
if (!targetType.options.keepDiscriminatorProperty) delete subValue[targetType.options.discriminator.property];
52+
}
53+
if (this.transformationType === TransformationType.CLASS_TO_CLASS) {
54+
realTargetType = subValue.constructor;
55+
}
56+
if (this.transformationType === TransformationType.CLASS_TO_PLAIN) {
57+
subValue[targetType.options.discriminator.property] = targetType.options.discriminator.subTypes.find((subType) => subType.value === subValue.constructor).name;
58+
}
59+
} else {
60+
realTargetType = targetType;
61+
}
62+
const value = this.transform(subSource, subValue, realTargetType, undefined, subValue instanceof Map, level + 1);
63+
4464
if (newValue instanceof Set) {
4565
newValue.add(value);
4666
} else {
@@ -55,7 +75,6 @@ export class TransformOperationExecutor {
5575
}
5676
});
5777
return newValue;
58-
5978
} else if (targetType === String && !isMap) {
6079
if (value === null || value === undefined)
6180
return value;
@@ -91,7 +110,7 @@ export class TransformOperationExecutor {
91110
this.recursionStack.add(value);
92111
}
93112

94-
const keys = this.getKeys(targetType, value);
113+
const keys = this.getKeys((targetType as Function), value);
95114
let newValue: any = source ? source : {};
96115
if (!source && (this.transformationType === TransformationType.PLAIN_TO_CLASS || this.transformationType === TransformationType.CLASS_TO_CLASS)) {
97116
if (isMap) {
@@ -109,16 +128,17 @@ export class TransformOperationExecutor {
109128
let valueKey = key, newValueKey = key, propertyName = key;
110129
if (!this.options.ignoreDecorators && targetType) {
111130
if (this.transformationType === TransformationType.PLAIN_TO_CLASS) {
112-
const exposeMetadata = defaultMetadataStorage.findExposeMetadataByCustomName(targetType, key);
131+
const exposeMetadata = defaultMetadataStorage.findExposeMetadataByCustomName((targetType as Function), key);
113132
if (exposeMetadata) {
114133
propertyName = exposeMetadata.propertyName;
115134
newValueKey = exposeMetadata.propertyName;
116135
}
117136

118137
} else if (this.transformationType === TransformationType.CLASS_TO_PLAIN || this.transformationType === TransformationType.CLASS_TO_CLASS) {
119-
const exposeMetadata = defaultMetadataStorage.findExposeMetadata(targetType, key);
120-
if (exposeMetadata && exposeMetadata.options && exposeMetadata.options.name)
138+
const exposeMetadata = defaultMetadataStorage.findExposeMetadata((targetType as Function), key);
139+
if (exposeMetadata && exposeMetadata.options && exposeMetadata.options.name) {
121140
newValueKey = exposeMetadata.options.name;
141+
}
122142
}
123143
}
124144

@@ -138,10 +158,30 @@ export class TransformOperationExecutor {
138158
type = targetType;
139159

140160
} else if (targetType) {
141-
const metadata = defaultMetadataStorage.findTypeMetadata(targetType, propertyName);
161+
162+
const metadata = defaultMetadataStorage.findTypeMetadata((targetType as Function), propertyName);
142163
if (metadata) {
143-
const options: TypeOptions = {newObject: newValue, object: value, property: propertyName};
144-
type = metadata.typeFunction(options);
164+
const options: TypeHelpOptions = { newObject: newValue, object: value, property: propertyName };
165+
const newType = metadata.typeFunction(options);
166+
if (metadata.options && metadata.options.discriminator && metadata.options.discriminator.property && metadata.options.discriminator.subTypes) {
167+
if (!(value[valueKey] instanceof Array)) {
168+
if (this.transformationType === TransformationType.PLAIN_TO_CLASS) {
169+
type = metadata.options.discriminator.subTypes.find((subType) => subType.name === subValue[metadata.options.discriminator.property]);
170+
type === undefined ? type = newType : type = type.value;
171+
if (!metadata.options.keepDiscriminatorProperty) delete subValue[metadata.options.discriminator.property];
172+
}
173+
if (this.transformationType === TransformationType.CLASS_TO_CLASS) {
174+
type = subValue.constructor;
175+
}
176+
if (this.transformationType === TransformationType.CLASS_TO_PLAIN) {
177+
subValue[metadata.options.discriminator.property] = metadata.options.discriminator.subTypes.find((subType) => subType.value === subValue.constructor).name;
178+
}
179+
} else {
180+
type = metadata;
181+
}
182+
} else {
183+
type = newType;
184+
}
145185
isSubValueMap = isSubValueMap || metadata.reflectedType === Map;
146186
} else if (this.options.targetMaps) { // try to find a type in target maps
147187
this.options.targetMaps
@@ -151,7 +191,8 @@ export class TransformOperationExecutor {
151191
}
152192

153193
// if value is an array try to get its custom array type
154-
const arrayType = Array.isArray(value[valueKey]) ? this.getReflectedType(targetType, propertyName) : undefined;
194+
const arrayType = Array.isArray(value[valueKey]) ? this.getReflectedType((targetType as Function), propertyName) : undefined;
195+
155196
// const subValueKey = TransformationType === TransformationType.PLAIN_TO_CLASS && newKeyName ? newKeyName : key;
156197
const subSource = source ? source[valueKey] : undefined;
157198

@@ -176,14 +217,14 @@ export class TransformOperationExecutor {
176217
// Get original value
177218
finalValue = value[transformKey];
178219
// Apply custom transformation
179-
finalValue = this.applyCustomTransformations(finalValue, targetType, transformKey, value, this.transformationType);
220+
finalValue = this.applyCustomTransformations(finalValue, (targetType as Function), transformKey, value, this.transformationType);
180221
// If nothing change, it means no custom transformation was applied, so use the subValue.
181222
finalValue = (value[transformKey] === finalValue) ? subValue : finalValue;
182223
// Apply the default transformation
183224
finalValue = this.transform(subSource, finalValue, type, arrayType, isSubValueMap, level + 1);
184225
} else {
185226
finalValue = this.transform(subSource, subValue, type, arrayType, isSubValueMap, level + 1);
186-
finalValue = this.applyCustomTransformations(finalValue, targetType, transformKey, value, this.transformationType);
227+
finalValue = this.applyCustomTransformations(finalValue, (targetType as Function), transformKey, value, this.transformationType);
187228
}
188229

189230
if (newValue instanceof Map) {
@@ -193,7 +234,7 @@ export class TransformOperationExecutor {
193234
}
194235
} else if (this.transformationType === TransformationType.CLASS_TO_CLASS) {
195236
let finalValue = subValue;
196-
finalValue = this.applyCustomTransformations(finalValue, targetType, key, value, this.transformationType);
237+
finalValue = this.applyCustomTransformations(finalValue, (targetType as Function), key, value, this.transformationType);
197238
if (newValue instanceof Map) {
198239
newValue.set(newValueKey, finalValue);
199240
} else {

src/decorators.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import {ClassTransformer} from "./ClassTransformer";
22
import {defaultMetadataStorage} from "./storage";
33
import {TypeMetadata} from "./metadata/TypeMetadata";
44
import {ExposeMetadata} from "./metadata/ExposeMetadata";
5-
import {ExposeOptions, ExcludeOptions, TypeOptions, TransformOptions} from "./metadata/ExposeExcludeOptions";
5+
import {ExposeOptions, ExcludeOptions, TypeHelpOptions, TransformOptions, Discriminator, TypeOptions} from "./metadata/ExposeExcludeOptions";
66
import {ExcludeMetadata} from "./metadata/ExcludeMetadata";
77
import {TransformMetadata} from "./metadata/TransformMetadata";
88
import {ClassTransformOptions} from "./ClassTransformOptions";
@@ -20,11 +20,12 @@ 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. A discriminator can be given in the options.
2324
*/
24-
export function Type(typeFunction?: (type?: TypeOptions) => Function) {
25+
export function Type(typeFunction: (type?: TypeHelpOptions) => Function, options?: TypeOptions) {
2526
return function(target: any, key: string) {
2627
const type = (Reflect as any).getMetadata("design:type", target, key);
27-
const metadata = new TypeMetadata(target.constructor, key, type, typeFunction);
28+
const metadata = new TypeMetadata(target.constructor, key, type, typeFunction, options);
2829
defaultMetadataStorage.addTypeMetadata(metadata);
2930
};
3031
}

src/metadata/ExposeExcludeOptions.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,14 @@ export interface TransformOptions {
88
}
99

1010
export interface TypeOptions {
11+
discriminator?: Discriminator;
12+
/**
13+
* Is false by default.
14+
*/
15+
keepDiscriminatorProperty?: boolean;
16+
}
17+
18+
export interface TypeHelpOptions {
1119
newObject: any;
1220
object: Object;
1321
property: string;
@@ -26,3 +34,13 @@ export interface ExcludeOptions {
2634
toClassOnly?: boolean;
2735
toPlainOnly?: boolean;
2836
}
37+
38+
export interface Discriminator {
39+
property: string;
40+
subTypes: JsonSubType[];
41+
}
42+
43+
export interface JsonSubType {
44+
value: new (...args: any[]) => any;
45+
name: string;
46+
}

src/metadata/TypeMetadata.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
1-
import {TypeOptions} from "./ExposeExcludeOptions";
1+
import {TypeHelpOptions, Discriminator, TypeOptions} from "./ExposeExcludeOptions";
22

33
export class TypeMetadata {
44

55
constructor(public target: Function,
66
public propertyName: string,
77
public reflectedType: any,
8-
public typeFunction: (options?: TypeOptions) => Function) {
8+
public typeFunction: (options?: TypeHelpOptions) => Function,
9+
public options: TypeOptions) {
910
}
1011

11-
}
12+
}

0 commit comments

Comments
 (0)