Skip to content

Instantly share code, notes, and snippets.

@jeongtae
Last active January 14, 2024 17:39
Show Gist options
  • Select an option

  • Save jeongtae/f65ddd8f17f8c388659aab76890f194b to your computer and use it in GitHub Desktop.

Select an option

Save jeongtae/f65ddd8f17f8c388659aab76890f194b to your computer and use it in GitHub Desktop.
create-map-transform-fn

create-map-transform-fn

Utility function to solve issue#288 of class-transformer: ES6 Maps are not constructed properly

๐Ÿ“ฆ Install

npm i gist:f65ddd8f17f8c388659aab76890f194b
# or
yarn add gist:f65ddd8f17f8c388659aab76890f194b

๐Ÿ˜‹ Usage

import { ToMap } from "create-map-transform-fn";

class Item {
  title: string;

  @ToMap({ mapValueClass: Number })
  options: Map<string, number> = new Map();
}

class Order {
  @ToMap({ mapValueClass: Item })
  items: Map<string, Item> = new Map();
}

โœ‹ Notes

  • The type of Map's key must be string. (e.g. Map<string, Foo>)
  • If the type of Map's value is primitive types(String, Number, Boolean), the corresponding wrapper function(String(value), Number(value), Boolean(value)) will be called with the value as a parameter.
"use strict";
var __assign = (this && this.__assign) || function () {
__assign = Object.assign || function(t) {
for (var s, i = 1, n = arguments.length; i < n; i++) {
s = arguments[i];
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p))
t[p] = s[p];
}
return t;
};
return __assign.apply(this, arguments);
};
exports.__esModule = true;
exports.ToMap = void 0;
var class_transformer_1 = require("class-transformer");
function isPrimitiveType(mapValueClass) {
return [String, Number, Boolean].includes(mapValueClass);
}
/**
* Create a transformation function for `Transform` decorator which decorates `Map<string, any>` type property.
* @param mapValueClass Type of value. (e.g. `MyClass`, `Number`, `String`, `Boolean` ...)
* @deprecated Use `ToMap` instead.
*/
function createMapTransformFn(mapValueClass) {
return function (_a) {
var type = _a.type, value = _a.value, options = _a.options;
var isPrimitiveValueClass = isPrimitiveType(mapValueClass);
if (type === class_transformer_1.TransformationType.PLAIN_TO_CLASS) {
if (value instanceof Object === false) {
return new Map();
}
var transformedEntries = Object.entries(value)
.filter(function (_a) {
var v = _a[1];
return isPrimitiveValueClass || typeof v === "object";
})
.map(function (_a) {
var k = _a[0], v = _a[1];
return [k, isPrimitiveValueClass ? mapValueClass(v) : (0, class_transformer_1.plainToInstance)(mapValueClass, v, options)];
});
return new Map(transformedEntries);
}
if (type === class_transformer_1.TransformationType.CLASS_TO_PLAIN) {
if (value instanceof Map === false) {
return {};
}
var transformedEntries = Array.from(value.entries())
.filter(function (_a) {
var k = _a[0], v = _a[1];
return typeof k === "string" && (isPrimitiveValueClass || v instanceof mapValueClass);
})
.map(function (_a) {
var k = _a[0], v = _a[1];
return [k, isPrimitiveValueClass ? mapValueClass(v) : (0, class_transformer_1.instanceToPlain)(v, options)];
});
return Object.fromEntries(transformedEntries);
}
return value;
};
}
exports["default"] = createMapTransformFn;
/**
* Defines that it will be converted to a Map.
* Can be applied to properties only.
* @param mapValueClass Type of value. (e.g. `MyClass`, `Number`, `String`, `Boolean` ...)
* @deprecated Use `ToMap` instead.
*/
function ToMap(_a, transformOptions) {
var mapValueClass = _a.mapValueClass;
return (0, class_transformer_1.Transform)(createMapTransformFn(mapValueClass), __assign(__assign({}, transformOptions), { toClassOnly: true, toPlainOnly: false }));
}
exports.ToMap = ToMap;
var __assign = (this && this.__assign) || function () {
__assign = Object.assign || function(t) {
for (var s, i = 1, n = arguments.length; i < n; i++) {
s = arguments[i];
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p))
t[p] = s[p];
}
return t;
};
return __assign.apply(this, arguments);
};
import { instanceToPlain, plainToInstance, Transform, TransformationType } from "class-transformer";
function isPrimitiveType(mapValueClass) {
return [String, Number, Boolean].includes(mapValueClass);
}
/**
* Create a transformation function for `Transform` decorator which decorates `Map<string, any>` type property.
* @param mapValueClass Type of value. (e.g. `MyClass`, `Number`, `String`, `Boolean` ...)
* @deprecated Use `ToMap` instead.
*/
export default function createMapTransformFn(mapValueClass) {
return function (_a) {
var type = _a.type, value = _a.value, options = _a.options;
var isPrimitiveValueClass = isPrimitiveType(mapValueClass);
if (type === TransformationType.PLAIN_TO_CLASS) {
if (value instanceof Object === false) {
return new Map();
}
var transformedEntries = Object.entries(value)
.filter(function (_a) {
var v = _a[1];
return isPrimitiveValueClass || typeof v === "object";
})
.map(function (_a) {
var k = _a[0], v = _a[1];
return [k, isPrimitiveValueClass ? mapValueClass(v) : plainToInstance(mapValueClass, v, options)];
});
return new Map(transformedEntries);
}
if (type === TransformationType.CLASS_TO_PLAIN) {
if (value instanceof Map === false) {
return {};
}
var transformedEntries = Array.from(value.entries())
.filter(function (_a) {
var k = _a[0], v = _a[1];
return typeof k === "string" && (isPrimitiveValueClass || v instanceof mapValueClass);
})
.map(function (_a) {
var k = _a[0], v = _a[1];
return [k, isPrimitiveValueClass ? mapValueClass(v) : instanceToPlain(v, options)];
});
return Object.fromEntries(transformedEntries);
}
return value;
};
}
/**
* Defines that it will be converted to a Map.
* Can be applied to properties only.
* @param mapValueClass Type of value. (e.g. `MyClass`, `Number`, `String`, `Boolean` ...)
* @deprecated Use `ToMap` instead.
*/
export function ToMap(_a, transformOptions) {
var mapValueClass = _a.mapValueClass;
return Transform(createMapTransformFn(mapValueClass), __assign(__assign({}, transformOptions), { toClassOnly: true, toPlainOnly: false }));
}
/** @type {import('ts-jest').JestConfigWithTsJest} */
module.exports = {
preset: "ts-jest",
testEnvironment: "node",
};
{
"name": "create-map-transform-fn",
"version": "0.3.0",
"repository": "https://gist.github.com/jeongtae/f65ddd8f17f8c388659aab76890f194b",
"author": {
"name": "Jeongtae Kim",
"url": "https://github.com/jeongtae"
},
"contributors": [
{
"name": "Coroliov Oleg",
"url": "https://github.com/ruscon"
}
],
"types": "./types.d.ts",
"main": "./cjs.js",
"module": "./esm.js",
"source": "./src.ts",
"scripts": {
"build:esm": "npx tsc -m esnext --outDir . && mv src.js esm.js",
"build:cjs": "npx tsc -m commonjs --outDir . && mv src.js cjs.js",
"build:types": "npx tsc -d --emitDeclarationOnly --outDir . && mv src.d.ts types.d.ts",
"build": "npm run build:esm && npm run build:cjs && npm run build:types",
"test": "jest"
},
"devDependencies": {
"@types/jest": "^29.5.5",
"ts-jest": "^29.1.1",
"typescript": "^4.9.5"
},
"peerDependencies": {
"class-transformer": "^0.5.1"
}
}
import { Expose, instanceToPlain, plainToInstance } from "class-transformer";
import { ToMap } from "./src";
class Item {
constructor(prop: number) {
this.prop = prop;
}
@Expose()
prop!: number;
}
class TestNumberModel {
@Expose()
@ToMap({ mapValueClass: Number })
prop!: Map<string, number>;
}
class TestStringModel {
@Expose()
@ToMap({ mapValueClass: String })
prop!: Map<string, string>;
}
class TestBooleanModel {
@Expose()
@ToMap({ mapValueClass: Boolean })
prop!: Map<string, boolean>;
}
class TestItemModel {
@Expose()
@ToMap({ mapValueClass: Item })
prop!: Map<string, Item>;
}
describe(ToMap.name, () => {
describe("should work with Number value class", () => {
const plain = {
prop: {
"1": 11,
"2": 22,
},
};
it("should transform Record<string, number> to Map<string, number>", () => {
return expect(plainToInstance(TestNumberModel, plain).prop).toEqual(
(new TestNumberModel().prop = new Map([
["1", 11],
["2", 22],
]))
);
});
it("should transform Map<string, number> to Record<string, number>", () => {
return expect(instanceToPlain(plainToInstance(TestNumberModel, plain))).toEqual(plain);
});
});
describe("should work with String value class", () => {
const plain = {
prop: {
"1": "11",
"2": "22",
},
};
it("should transform Record<string, string> to Map<string, string>", () => {
return expect(plainToInstance(TestStringModel, plain).prop).toEqual(
(new TestStringModel().prop = new Map([
["1", "11"],
["2", "22"],
]))
);
});
it("should transform Map<string, string> to Record<string, string>", () => {
return expect(instanceToPlain(plainToInstance(TestStringModel, plain))).toEqual(plain);
});
});
describe("should work with Boolean value class", () => {
const plain = {
prop: {
"1": true,
"2": false,
},
};
it("should transform Record<string, boolean> to Map<string, boolean>", () => {
return expect(plainToInstance(TestBooleanModel, plain).prop).toEqual(
(new TestBooleanModel().prop = new Map([
["1", true],
["2", false],
]))
);
});
it("should transform Map<string, boolean> to Record<string, boolean>", () => {
return expect(instanceToPlain(plainToInstance(TestBooleanModel, plain))).toEqual(plain);
});
});
describe("should work with custom value class", () => {
const plain = {
prop: {
"1": { prop: 11 },
"2": { prop: 22 },
},
};
it("should transform Record<string, Item> to Map<string, Item>", () => {
return expect(plainToInstance(TestItemModel, plain).prop).toEqual(
(new TestItemModel().prop = new Map([
["1", new Item(11)],
["2", new Item(22)],
]))
);
});
it("should transform Map<string, Item> to Record<string, Item>", () => {
return expect(instanceToPlain(plainToInstance(TestItemModel, plain))).toEqual(plain);
});
});
});
import type { ClassConstructor, TransformFnParams, TransformOptions } from "class-transformer";
import { instanceToPlain, plainToInstance, Transform, TransformationType } from "class-transformer";
type PrimitiveType = StringConstructor | NumberConstructor | BooleanConstructor;
export interface IToMapOptions<T> {
/**
* @default comma
*/
mapValueClass: PrimitiveType | ClassConstructor<T>;
}
function isPrimitiveType(mapValueClass: unknown): mapValueClass is PrimitiveType {
return [String, Number, Boolean].includes(mapValueClass as PrimitiveType);
}
/**
* Create a transformation function for `Transform` decorator which decorates `Map<string, any>` type property.
* @param mapValueClass Type of value. (e.g. `MyClass`, `Number`, `String`, `Boolean` ...)
* @deprecated Use `ToMap` instead.
*/
export default function createMapTransformFn<T>(mapValueClass: PrimitiveType | ClassConstructor<T>) {
return ({ type, value, options }: TransformFnParams): any => {
const isPrimitiveValueClass = isPrimitiveType(mapValueClass);
if (type === TransformationType.PLAIN_TO_CLASS) {
if (value instanceof Object === false) {
return new Map();
}
const transformedEntries = Object.entries(value)
.filter(([, v]) => {
return isPrimitiveValueClass || typeof v === "object";
})
.map(([k, v]) => {
return [k, isPrimitiveValueClass ? (mapValueClass as any)(v) : plainToInstance(mapValueClass, v, options)];
}) as [string, T][];
return new Map(transformedEntries);
}
if (type === TransformationType.CLASS_TO_PLAIN) {
if (value instanceof Map === false) {
return {};
}
const transformedEntries = Array.from((value as Map<string, T>).entries())
.filter(([k, v]) => {
return typeof k === "string" && (isPrimitiveValueClass || v instanceof mapValueClass);
})
.map(([k, v]) => {
return [k, isPrimitiveValueClass ? (mapValueClass as any)(v) : instanceToPlain(v, options)];
});
return Object.fromEntries(transformedEntries);
}
return value;
};
}
/**
* Defines that it will be converted to a Map.
* Can be applied to properties only.
* @param mapValueClass Type of value. (e.g. `MyClass`, `Number`, `String`, `Boolean` ...)
*/
export function ToMap<T>(
{ mapValueClass }: IToMapOptions<T>,
transformOptions?: Omit<TransformOptions, "toClassOnly" | "toPlainOnly">
): PropertyDecorator {
return Transform(createMapTransformFn(mapValueClass), {
...transformOptions,
toClassOnly: true,
toPlainOnly: false,
});
}
{
"files": ["src.ts"],
"compilerOptions": {
"moduleResolution": "Node",
"lib": ["ESNext"],
"experimentalDecorators": true,
"outDir": "./dist",
"strict": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"esModuleInterop": true
}
}
import type { ClassConstructor, TransformFnParams, TransformOptions } from "class-transformer";
type PrimitiveType = StringConstructor | NumberConstructor | BooleanConstructor;
export interface IToMapOptions<T> {
/**
* @default comma
*/
mapValueClass: PrimitiveType | ClassConstructor<T>;
}
/**
* Create a transformation function for `Transform` decorator which decorates `Map<string, any>` type property.
* @param mapValueClass Type of value. (e.g. `MyClass`, `Number`, `String`, `Boolean` ...)
* @deprecated Use `ToMap` instead.
*/
export default function createMapTransformFn<T>(mapValueClass: PrimitiveType | ClassConstructor<T>): ({ type, value, options }: TransformFnParams) => any;
/**
* Defines that it will be converted to a Map.
* Can be applied to properties only.
* @param mapValueClass Type of value. (e.g. `MyClass`, `Number`, `String`, `Boolean` ...)
*/
export declare function ToMap<T>({ mapValueClass }: IToMapOptions<T>, transformOptions?: Omit<TransformOptions, "toClassOnly" | "toPlainOnly">): PropertyDecorator;
export {};
@jeongtae
Copy link
Author

@ruscon

Thank you for your comment. ๐Ÿ™‚
I've created a new revision based on the refactored version you wrote.

And I also put your name in the contributors section of package.json.

Besides I'm realizing that it's better to maintain this in npm rather than gist. ๐Ÿ˜†

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment