├── .gitignore ├── LICENSE ├── README.md ├── jest.config.js ├── package-lock.json ├── package.json ├── src ├── __tests__ │ ├── jenum.test.ts │ ├── jenum_create_class_tool.test.ts │ ├── jenum_json_tools.test.ts │ └── tsconfig.json ├── example │ ├── examples.ts │ └── tsconfig.json └── ts │ └── jenum.ts ├── tsconfig.json ├── tsconfig.test.json └── tslint.json /.gitignore: -------------------------------------------------------------------------------- 1 | # eclipse 2 | .project 3 | .classpath 4 | .settings 5 | target/ 6 | 7 | node_modules/ 8 | dist/ 9 | ts-jenum-*.tgz 10 | 11 | # idea 12 | .idea 13 | *.iml 14 | *.iws 15 | *.ipr 16 | 17 | # java 18 | *.class -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 RefOrms 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ts-jenum 2 | * TypeScript Enum like java.lang.Enum 3 | * EnumType 4 | * Enum 5 | * Powerful tool to comfortable work with plain json struct like enum 6 | * EnumTools 7 | 8 | # Installation 9 | npm i ts-jenum 10 | 11 | # Example in TypeScript 12 | ## TypeScript Enum like java.lang.Enum 13 | ```typescript 14 | import {Enum, EnumType} from "ts-jenum"; 15 | 16 | @Enum("text") 17 | export class State extends EnumType() { 18 | 19 | static readonly NEW = new State(1, "New"); 20 | static readonly ACTIVE = new State(2, "Active"); 21 | static readonly BLOCKED = new State(3, "Blocked"); 22 | 23 | private constructor(readonly code: number, readonly text: string) { 24 | super(); 25 | } 26 | } 27 | 28 | // Usage example 29 | console.log("" + State.ACTIVE); // "Active" 30 | console.log("" + State.BLOCKED); // "Blocked" 31 | console.log(State.values()); // [State.NEW, State.ACTIVE, State.BLOCKED] 32 | console.log(State.valueOf("New")); // State.NEW 33 | State.valueOf("Unknown") // throw Error(...) 34 | console.log(State.valueByName("NEW")); // State.NEW 35 | console.log(State.ACTIVE.enumName); // ACTIVE 36 | 37 | const first = state => state.code === 1; 38 | console.log(State.find("New")); // State.NEW 39 | console.log(State.find(first)); // State.NEW 40 | console.log(State.find("Unknown")); // null 41 | const last = state => state.code === 3; 42 | console.log(State.filter(last)) // [State.BLOCKED] 43 | console.log(State.keys()) // ["NEW", "ACTIVE", "BLOCKED"] 44 | 45 | // be "NEW" | "ACTIVE" | "BLOCKED" 46 | type StateNameUnion = EnumConstNames; 47 | 48 | ``` 49 | ## EnumTools powerful tool to comfortable work with plain json struct like enum 50 | ```typescript 51 | import {EnumTools} from "ts-jenum"; 52 | // plain json like enum 53 | const Colors = { 54 | WHITE: "#FFFFFF", 55 | GRAY: "#808080", 56 | BLACK: "#000000" 57 | }; 58 | 59 | // to be ["WHITE", "GRAY", "BLACK"] 60 | const keys = EnumTools.keys(Colors); 61 | 62 | // to be ["#FFFFFF", "#808080", "#000000"] 63 | const values = EnumTools.values(Colors); 64 | 65 | /** 66 | * to be { 67 | * "#FFFFFF": "WHITE", 68 | * "#808080": "GRAY", 69 | * "#000000": "BLACK" 70 | * }; 71 | */ 72 | const rStruct = EnumTools.reverse(Colors); 73 | 74 | /** 75 | * to be: [ 76 | * {key: "WHITE", value: "#FFFFFF"}, 77 | * {key: "GRAY", value: "#808080"}, 78 | * {key: "BLACK", value: "#000000"} 79 | * ] 80 | */ 81 | const pairs = EnumTools.pairs(Colors); 82 | 83 | /** 84 | * To be class like: 85 | * @Enum("key") 86 | * class ColorEnum extends EnumType() { 87 | * static readonly WHITE = new ColorEnum("WHITE", "#FFFFFF"); 88 | * static readonly GRAY = new ColorEnum("GRAY", "#808080"); 89 | * static readonly BLACK = new ColorEnum("BLACK", "#000000"); 90 | * private constructor(readonly key: string, readonly value: string | number) { 91 | * super(); 92 | * } 93 | * } 94 | * ColorEnum has all IDE hint for developer, type checking and type safety 95 | */ 96 | const ColorEnum = EnumTools.toClass(Colors); 97 | 98 | ``` 99 | 100 | Details. Type safety. 101 | In example above, you can write "tExt" or "txt" instead of "text" as @Enum decorator argument and no exception happen. In example below this problem is absent. Add an expression <State> to @Enum decorator 102 | 103 | ```typescript 104 | import {Enum, EnumConstNames, EnumType} from "ts-jenum"; 105 | 106 | @Enum("text") 107 | export class State extends EnumType() { 108 | 109 | static readonly NEW = new State(1, "New"); 110 | static readonly ACTIVE = new State(2, "Active"); 111 | static readonly BLOCKED = new State(3, "Blocked"); 112 | 113 | private constructor(readonly code: number, readonly text: string) { 114 | super(); 115 | } 116 | } 117 | // Get Enum Names 118 | // be "NEW" | "ACTIVE" | "BLOCKED" 119 | type StateNameUnion = EnumConstNames; 120 | ``` 121 | 122 | Powerful typing. 123 | 124 | ```typescript 125 | @Enum("id") 126 | class Person extends EnumType() { 129 | 130 | static readonly IVAN = new Person(1 as const, "Ivan" as const); 131 | static readonly JOHN = new Person(2 as const, "John" as const); 132 | 133 | private constructor(readonly id: IdType, readonly name: NameType) { 134 | super(); 135 | } 136 | 137 | static doSomeWork(): void { 138 | // type to be: "Ivan". Not string! 139 | const name = Person.IVAN.name; 140 | // to be: error 141 | // if (name === "cat") 142 | // ^ This condition will always return 'false' since the types '"Ivan"' and '"cat"' have no overlap. 143 | 144 | // type to be: 1. Not number! 145 | const id = Person.IVAN.id; 146 | // to be: error 147 | // if (id === 3) 148 | // ^ This condition will always return 'false' since the types '1' and '3' have no overlap 149 | } 150 | } 151 | ``` -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "roots": [ 3 | "./src" 4 | ], 5 | "transform": { 6 | "^.+\\.tsx?$": "ts-jest" 7 | }, 8 | "globals": { 9 | "ts-jest": { 10 | "tsConfig": "tsconfig.test.json", 11 | "diagnostics": { 12 | "ignoreCodes": [151001] 13 | } 14 | } 15 | }, 16 | "moduleDirectories": ['node_modules', 'src'] 17 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ts-jenum", 3 | "version": "2.2.2", 4 | "description": "TypeScript Java Enum", 5 | "repository": { 6 | "type": "git", 7 | "url": "https://github.com/reforms/ts-jenum.git" 8 | }, 9 | "homepage": "https://github.com/reforms/ts-jenum", 10 | "bugs": { 11 | "url": "https://github.com/reforms/ts-jenum/issues" 12 | }, 13 | "files": [ 14 | "dist", 15 | "src" 16 | ], 17 | "main": "dist/commonjs/jenum.js", 18 | "jsnext:main": "dist/es/jenum.js", 19 | "module": "dist/es/jenum.js", 20 | "types": "dist/types/jenum.d.ts", 21 | "scripts": { 22 | "clean:dist": "rimraf dist", 23 | "clean:pack": "rimraf ts-enum-util-*.tgz", 24 | "compile": "tsc --project tsconfig.json --noEmit --pretty --noErrorTruncation", 25 | "lint": "tslint --config tslint.json --project tsconfig.json", 26 | "lint:fix": "npm run lint -- --fix", 27 | "build:types": "tsc --project tsconfig.json --pretty --noErrorTruncation --emitDeclarationOnly true --declarationMap true --outDir dist/types", 28 | "build:commonjs": "tsc --project tsconfig.json --pretty --noErrorTruncation --declaration false --outDir dist/commonjs", 29 | "build:es": "tsc --project tsconfig.json --pretty --noErrorTruncation --declaration false -m es6 --outDir dist/es", 30 | "build": "npm run clean:dist && run-p build:types build:es build:commonjs && npm run test", 31 | "pack": "run-p clean:pack build && npm pack", 32 | "test": "jest -c jest.config.js --verbose", 33 | "dtslint": "run-s clean:dist build:types dtslint:v2_8_plus" 34 | }, 35 | "author": { 36 | "name": "RefOrms", 37 | "email": "reforms2017@gmail.com" 38 | }, 39 | "license": "MIT", 40 | "devDependencies": { 41 | "@types/jest": "^24.0.21", 42 | "@types/node": "10.3.0", 43 | "dtslint": "0.3.0", 44 | "npm-run-all": "4.1.3", 45 | "prettier": "1.13.4", 46 | "jest": "^24.9.0", 47 | "rimraf": "2.6.2", 48 | "tslint": "5.10.0", 49 | "ts-jest": "^24.1.0", 50 | "tslint-config-prettier": "1.13.0", 51 | "typescript": "2.9.2" 52 | }, 53 | "peerDependencies": { 54 | "typescript": ">= 2.2.2" 55 | }, 56 | "keywords": [ 57 | "typescript", 58 | "string", 59 | "number", 60 | "enum", 61 | "java" 62 | ] 63 | } 64 | -------------------------------------------------------------------------------- /src/__tests__/jenum.test.ts: -------------------------------------------------------------------------------- 1 | import {Enum, EnumType, SearchPredicate} from "../ts/jenum"; 2 | 3 | describe("EnumApi", () => { 4 | 5 | test("values", () => { 6 | const states = State.values(); 7 | expect(states.length).toBe(3); 8 | expect(states[0]).toBe(State.NEW); 9 | expect(states[1]).toBe(State.ACTIVE); 10 | expect(states[2]).toBe(State.BLOCKED); 11 | }); 12 | 13 | test("keys", () => { 14 | const enumNames = State.keys(); 15 | expect(enumNames.length).toBe(3); 16 | expect(enumNames[0]).toBe("NEW"); 17 | expect(enumNames[1]).toBe("ACTIVE"); 18 | expect(enumNames[2]).toBe("BLOCKED"); 19 | }); 20 | 21 | test("valueOf", () => { 22 | expect(State.valueOf("New")).toBe(State.NEW); 23 | expect(State.valueOf("Active")).toBe(State.ACTIVE); 24 | expect(State.valueOf("Blocked")).toBe(State.BLOCKED); 25 | }); 26 | 27 | test("valueOf$error", () => { 28 | expect(() => State.valueOf("Unknown")).toThrowError(); 29 | }); 30 | 31 | test("valueByName", () => { 32 | expect(State.valueByName("NEW")).toBe(State.NEW); 33 | expect(State.valueByName("ACTIVE")).toBe(State.ACTIVE); 34 | expect(State.valueByName("BLOCKED")).toBe(State.BLOCKED); 35 | }); 36 | 37 | test("valueByName$error", () => { 38 | expect(() => State.valueByName("Unknown")).toThrowError(); 39 | }); 40 | 41 | test("find$usingId", () => { 42 | expect(State.find("New")).toBe(State.NEW); 43 | expect(State.find("Active")).toBe(State.ACTIVE); 44 | expect(State.find("Blocked")).toBe(State.BLOCKED); 45 | expect(State.find("Unknown")).toBeNull(); 46 | }); 47 | 48 | test("find$usingPredicate", () => { 49 | expect(State.find(state => state.code === 1)).toBe(State.NEW); 50 | expect(State.find(state => state.code === 2)).toBe(State.ACTIVE); 51 | expect(State.find(state => state.code === 3)).toBe(State.BLOCKED); 52 | expect(State.find(state => state.code === 4)).toBeNull(); 53 | }); 54 | 55 | test("filter", () => { 56 | expect(State.filter(state => state.code === 1)[0]).toBe(State.NEW); 57 | expect(State.filter(state => state.code === 2)[0]).toBe(State.ACTIVE); 58 | expect(State.filter(state => state.code === 3)[0]).toBe(State.BLOCKED); 59 | expect(State.filter(state => state.code === 4).length).toBe(0); 60 | const checkFilter = (predicate: SearchPredicate, expectedStatew: State[]) => { 61 | const actualStates = State.filter(predicate); 62 | expect(actualStates.length).toBe(expectedStatew.length); 63 | for (let index = 0; index < actualStates.length; index++) { 64 | expect(actualStates[index]).toBe(expectedStatew[index]); 65 | } 66 | } 67 | checkFilter(state => state.code > 1, [State.ACTIVE, State.BLOCKED]); 68 | checkFilter(state => state.code !== 2, [State.NEW, State.BLOCKED]); 69 | }); 70 | 71 | test("enumName", () => { 72 | expect(State.NEW.enumName).toBe("NEW"); 73 | expect(State.ACTIVE.enumName).toBe("ACTIVE"); 74 | expect(State.BLOCKED.enumName).toBe("BLOCKED"); 75 | }); 76 | 77 | test("toString", () => { 78 | expect(State.NEW.toString()).toBe("New"); 79 | expect(State.ACTIVE.toString()).toBe("Active"); 80 | expect(State.BLOCKED.toString()).toBe("Blocked"); 81 | }); 82 | }); 83 | 84 | @Enum("text") 85 | class State extends EnumType() { 86 | 87 | static readonly NEW = new State(1, "New"); 88 | static readonly ACTIVE = new State(2, "Active"); 89 | static readonly BLOCKED = new State(3, "Blocked"); 90 | 91 | private constructor(readonly code: number, readonly text: string) { 92 | super(); 93 | } 94 | } -------------------------------------------------------------------------------- /src/__tests__/jenum_create_class_tool.test.ts: -------------------------------------------------------------------------------- 1 | import {EnumTools, SearchPredicate, EnumType} from "../ts/jenum"; 2 | 3 | describe("EnumTools$toClass", () => { 4 | 5 | test("values", () => { 6 | const State = EnumTools.toClass(States); 7 | const states = State.values(); 8 | expect(states.length).toBe(3); 9 | expect(states[0]).toBe(State.NEW); 10 | expect(states[1]).toBe(State.ACTIVE); 11 | expect(states[2]).toBe(State.BLOCKED); 12 | }); 13 | 14 | test("keys", () => { 15 | const State = EnumTools.toClass(States); 16 | const enumNames = State.keys(); 17 | expect(enumNames.length).toBe(3); 18 | expect(enumNames[0]).toBe("NEW"); 19 | expect(enumNames[1]).toBe("ACTIVE"); 20 | expect(enumNames[2]).toBe("BLOCKED"); 21 | }); 22 | 23 | test("valueOf", () => { 24 | const State = EnumTools.toClass(States); 25 | expect(State.valueOf("NEW")).toBe(State.NEW); 26 | expect(State.valueOf("ACTIVE")).toBe(State.ACTIVE); 27 | expect(State.valueOf("BLOCKED")).toBe(State.BLOCKED); 28 | }); 29 | 30 | test("valueOf$error", () => { 31 | const State = EnumTools.toClass(States); 32 | expect(() => State.valueOf("Unknown")).toThrowError(); 33 | }); 34 | 35 | test("valueByName", () => { 36 | const State = EnumTools.toClass(States); 37 | expect(State.valueByName("NEW")).toBe(State.NEW); 38 | expect(State.valueByName("ACTIVE")).toBe(State.ACTIVE); 39 | expect(State.valueByName("BLOCKED")).toBe(State.BLOCKED); 40 | }); 41 | 42 | test("valueByName$error", () => { 43 | const State = EnumTools.toClass(States); 44 | expect(() => State.valueByName("Unknown")).toThrowError(); 45 | }); 46 | 47 | test("find$usingId", () => { 48 | const State = EnumTools.toClass(States); 49 | expect(State.find("NEW")).toBe(State.NEW); 50 | expect(State.find("ACTIVE")).toBe(State.ACTIVE); 51 | expect(State.find("BLOCKED")).toBe(State.BLOCKED); 52 | expect(State.find("Unknown")).toBeNull(); 53 | }); 54 | 55 | test("find$usingPredicate", () => { 56 | const State = EnumTools.toClass(States); 57 | expect(State.find(state => state.value === 1)).toBe(State.NEW); 58 | expect(State.find(state => state.value === 2)).toBe(State.ACTIVE); 59 | expect(State.find(state => state.value === 3)).toBe(State.BLOCKED); 60 | expect(State.find(state => state.value === 4)).toBeNull(); 61 | }); 62 | 63 | test("filter", () => { 64 | class State extends EnumTools.toClass(States) {} 65 | expect(State.filter(state => state.value === 1)[0]).toBe(State.NEW); 66 | expect(State.filter(state => state.value === 2)[0]).toBe(State.ACTIVE); 67 | expect(State.filter(state => state.value === 3)[0]).toBe(State.BLOCKED); 68 | expect(State.filter(state => state.value === 4).length).toBe(0); 69 | const checkFilter = (predicate: SearchPredicate, expectedStatew: State[]) => { 70 | const actualStates = State.filter(predicate); 71 | expect(actualStates.length).toBe(expectedStatew.length); 72 | for (let index = 0; index < actualStates.length; index++) { 73 | expect(actualStates[index]).toBe(expectedStatew[index]); 74 | } 75 | } 76 | checkFilter(state => state.value > 1, [State.ACTIVE, State.BLOCKED]); 77 | checkFilter(state => state.value !== 2, [State.NEW, State.BLOCKED]); 78 | }); 79 | 80 | test("enumName", () => { 81 | const State = EnumTools.toClass(States); 82 | expect(State.NEW.enumName).toBe("NEW"); 83 | expect(State.ACTIVE.enumName).toBe("ACTIVE"); 84 | expect(State.BLOCKED.enumName).toBe("BLOCKED"); 85 | }); 86 | 87 | test("toString", () => { 88 | const State = EnumTools.toClass(States); 89 | expect(State.NEW.toString()).toBe("NEW"); 90 | expect(State.ACTIVE.toString()).toBe("ACTIVE"); 91 | expect(State.BLOCKED.toString()).toBe("BLOCKED"); 92 | }); 93 | }); 94 | 95 | function assertPair(pair: { 96 | key: string; 97 | value: string | number; 98 | }, key: string, value: string | number) { 99 | expect(pair.key).toBe(key); 100 | expect(pair.value).toBe(value); 101 | } 102 | 103 | const States = { 104 | NEW: 1, 105 | ACTIVE: 2, 106 | BLOCKED: 3 107 | }; -------------------------------------------------------------------------------- /src/__tests__/jenum_json_tools.test.ts: -------------------------------------------------------------------------------- 1 | import {EnumTools} from "../ts/jenum"; 2 | 3 | describe("EnumTools$BaseApi", () => { 4 | 5 | test("keys$string", () => { 6 | const keys = EnumTools.keys(Colors); 7 | expect(keys.length).toBe(3); 8 | expect(keys[0]).toBe("WHITE"); 9 | expect(keys[1]).toBe("GRAY"); 10 | expect(keys[2]).toBe("BLACK"); 11 | }); 12 | 13 | test("keys$digit", () => { 14 | const keys = EnumTools.keys(DigitKeyColors); 15 | expect(keys.length).toBe(3); 16 | expect(keys[0]).toBe("1"); 17 | expect(keys[1]).toBe("2"); 18 | expect(keys[2]).toBe("3"); 19 | }); 20 | 21 | test("keys$mixed", () => { 22 | const keys = EnumTools.keys(MixedColors); 23 | expect(keys.length).toBe(4); 24 | expect(keys[0]).toBe("1"); 25 | expect(keys[1]).toBe("3"); 26 | expect(keys[2]).toBe("GRAY"); 27 | expect(keys[3]).toBe("DEFAULT"); 28 | }); 29 | 30 | test("values$string", () => { 31 | const values = EnumTools.values(Colors); 32 | expect(values.length).toBe(3); 33 | expect(values[0]).toBe("#FFFFFF"); 34 | expect(values[1]).toBe("#808080"); 35 | expect(values[2]).toBe("#000000"); 36 | }); 37 | 38 | test("values$digit", () => { 39 | const values = EnumTools.values(DigitValueColors); 40 | expect(values.length).toBe(3); 41 | expect(values[0]).toBe(255); 42 | expect(values[1]).toBe(127); 43 | expect(values[2]).toBe(0); 44 | }); 45 | 46 | test("values$mixed", () => { 47 | const values = EnumTools.values(MixedColors); 48 | expect(values.length).toBe(4); 49 | expect(values[0]).toBe(255); 50 | expect(values[1]).toBe("#000000"); 51 | expect(values[2]).toBe("#808080"); 52 | expect(values[3]).toBe(255); 53 | }); 54 | 55 | test("reverse$string", () => { 56 | const rStruct = EnumTools.reverse(Colors); 57 | expect(rStruct["#FFFFFF"]).toBe("WHITE"); 58 | expect(rStruct["#808080"]).toBe("GRAY"); 59 | expect(rStruct["#000000"]).toBe("BLACK"); 60 | }); 61 | 62 | test("reverse$digit", () => { 63 | const rStruct = EnumTools.reverse(DigitValueColors); 64 | expect(rStruct[255]).toBe("WHITE"); 65 | expect(rStruct[127]).toBe("GRAY"); 66 | expect(rStruct[0]).toBe("BLACK"); 67 | }); 68 | 69 | test("pairs$string", () => { 70 | const pairs = EnumTools.pairs(Colors); 71 | assertPair(pairs[0], "WHITE", "#FFFFFF"); 72 | assertPair(pairs[1], "GRAY", "#808080"); 73 | assertPair(pairs[2], "BLACK", "#000000"); 74 | }); 75 | 76 | test("pairs$digit", () => { 77 | const pairs = EnumTools.pairs(DigitValueColors); 78 | assertPair(pairs[0], "WHITE", 255); 79 | assertPair(pairs[1], "GRAY", 127); 80 | assertPair(pairs[2], "BLACK", 0); 81 | }); 82 | 83 | test("pairs$mixed", () => { 84 | const pairs = EnumTools.pairs(MixedColors); 85 | assertPair(pairs[0], "1", 255); 86 | assertPair(pairs[1], "3", "#000000"); 87 | assertPair(pairs[2], "GRAY", "#808080"); 88 | assertPair(pairs[3], "DEFAULT", 255); 89 | }); 90 | 91 | test("toClass$string", () => { 92 | const ColorEnum = EnumTools.toClass(Colors); 93 | const values = ColorEnum.values(); 94 | assertPair(values[0], "WHITE", "#FFFFFF"); 95 | }); 96 | }); 97 | 98 | function assertPair(pair: { 99 | key: string; 100 | value: string | number; 101 | }, key: string, value: string | number) { 102 | expect(pair.key).toBe(key); 103 | expect(pair.value).toBe(value); 104 | } 105 | 106 | const Colors = { 107 | WHITE: "#FFFFFF", 108 | GRAY: "#808080", 109 | BLACK: "#000000" 110 | }; 111 | 112 | const DigitValueColors = { 113 | WHITE: 255, 114 | GRAY: 127, 115 | BLACK: 0 116 | }; 117 | 118 | const DigitKeyColors = { 119 | 1: "#FFFFFF", 120 | 2: "#808080", 121 | 3: "#000000" 122 | }; 123 | 124 | const MixedColors = { 125 | 1: 255, 126 | GRAY: "#808080", 127 | 3: "#000000", 128 | DEFAULT: 255 129 | } -------------------------------------------------------------------------------- /src/__tests__/tsconfig.json: -------------------------------------------------------------------------------- 1 | /** Using by IDE */ 2 | { 3 | "compilerOptions": { 4 | "strict": true, 5 | "alwaysStrict": true, 6 | "noImplicitAny": true, 7 | "noImplicitReturns": true, 8 | "experimentalDecorators": true, 9 | "emitDecoratorMetadata": true, 10 | "preserveConstEnums": true, 11 | 12 | "skipLibCheck": true, 13 | } 14 | } -------------------------------------------------------------------------------- /src/example/examples.ts: -------------------------------------------------------------------------------- 1 | import {Enum, EnumType, EnumConstNames} from "../ts/jenum"; 2 | 3 | /** N1 ------------------------------------------------- **/ 4 | @Enum("text") 5 | class State extends EnumType() { 6 | 7 | static readonly NEW = new State(1, "New"); 8 | static readonly ACTIVE = new State(2, "Active"); 9 | static readonly BLOCKED = new State(3, "Blocked"); 10 | 11 | private constructor(readonly code: number, readonly text: string) { 12 | super(); 13 | } 14 | } 15 | // to be: "NEW" | "ACTIVE" | "BLOCKED" 16 | type StateNames = EnumConstNames; 17 | 18 | // to be: [State.NEW, State.ACTIVE, State.BLOCKED] 19 | const states = State.values(); 20 | 21 | // to be: ["NEW", "ACTIVE", "BLOCKED"] 22 | const enumNames = State.keys(); 23 | 24 | // to be: State.NEW 25 | const newState = State.valueByName("NEW") 26 | 27 | // to be: State.ACTIVE 28 | const activeState = State.valueOf("Active"); 29 | 30 | // to be: State.BLOCKED 31 | const blockedState = State.find("Blocked"); 32 | 33 | // to be: State.BLOCKED 34 | const blocedByCode3 = State.find(state => state.code === 3); 35 | 36 | // to be: [State.ACTIVE, State.BLOCKED] 37 | const someStates = State.filter(state => state.code >= 2); 38 | 39 | // to be: "Active" 40 | const activeText = State.ACTIVE.toString(); 41 | 42 | /** N2 ------------------------------------------------- **/ 43 | @Enum("id") 44 | class Person extends EnumType() { 47 | 48 | static readonly IVAN = new Person(1 as const, "Ivan" as const); 49 | static readonly JOHN = new Person(2 as const, "John" as const); 50 | 51 | private constructor(readonly id: IdType, readonly name: NameType) { 52 | super(); 53 | } 54 | 55 | static doSomeWork(): void { 56 | // type to be: "Ivan". Not string! 57 | const name = Person.IVAN.name; 58 | // to be: error 59 | // if (name === "ivan") 60 | // ^ This condition will always return 'false' since the types '"Ivan"' and '"cat"' have no overlap. 61 | 62 | // type to be: 1. Not string! 63 | const id = Person.IVAN.id; 64 | // to be: error 65 | // if (id === 3) 66 | // ^ This condition will always return 'false' since the types '1' and '3' have no overlap 67 | } 68 | } -------------------------------------------------------------------------------- /src/example/tsconfig.json: -------------------------------------------------------------------------------- 1 | /** Using by IDE */ 2 | { 3 | "compilerOptions": { 4 | "strict": true, 5 | "alwaysStrict": true, 6 | "noImplicitAny": true, 7 | "noImplicitReturns": true, 8 | "experimentalDecorators": true, 9 | "emitDecoratorMetadata": true, 10 | "preserveConstEnums": true, 11 | 12 | "skipLibCheck": true, 13 | } 14 | } -------------------------------------------------------------------------------- /src/ts/jenum.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Decorator for Enum. 3 | * @param {string} idPropertyName - property name to find enum value by property value. Usage in valueOf method 4 | * @return constructor of enum type 5 | */ 6 | export function Enum(idPropertyName?: keyof T) { 7 | // tslint:disable-next-line 8 | return function (target: T): T { 9 | const store: EnumStore = { 10 | name: target.prototype.constructor.name, 11 | enumMap: {}, 12 | enumMapByName: {}, 13 | enumValues: [], 14 | idPropertyName: idPropertyName 15 | }; 16 | // Lookup static fields 17 | for (const fieldName of Object.keys(target)) { 18 | const value: any = (target as any)[fieldName]; 19 | // Check static field: to be instance of enum type 20 | if (value instanceof target) { 21 | const enumItem: Enumerable = value; 22 | let id = fieldName; 23 | if (idPropertyName) { 24 | id = (value as any)[idPropertyName]; 25 | if (typeof id !== "string" && typeof id !== "number") { 26 | const enumName = store.name; 27 | throw new Error(`The value of the ${idPropertyName} property in the enumeration element ${enumName}.${fieldName} is not a string or a number: ${id}`); 28 | } 29 | } 30 | if (store.enumMap[id]) { 31 | const enumName = store.name; 32 | throw new Error(`An element with the identifier ${id}: ${enumName}.${store.enumMap[id].enumName} already exists in the enumeration ${enumName}`); 33 | } 34 | store.enumMap[id] = enumItem; 35 | store.enumMapByName[fieldName] = enumItem; 36 | store.enumValues.push(enumItem); 37 | enumItem.__enumName__ = fieldName; 38 | Object.freeze(enumItem); 39 | } 40 | } 41 | target.__store__ = store; 42 | Object.freeze(target.__store__); 43 | Object.freeze(target); 44 | return target; 45 | }; 46 | } 47 | 48 | /** Key->Value type */ 49 | type EnumMap = {[key: string]: Enumerable}; 50 | 51 | /** Type for Meta-Data of Enum */ 52 | type EnumClass = { 53 | __store__: EnumStore 54 | }; 55 | 56 | /** Store Type. Keep meta data for enum */ 57 | type EnumStore = { 58 | name: string, 59 | enumMap: EnumMap, 60 | enumMapByName: EnumMap, 61 | enumValues: Enumerable[], 62 | idPropertyName?: any 63 | }; 64 | 65 | /** Enum Item Type */ 66 | type EnumItemType = { 67 | __enumName__: string; 68 | }; 69 | 70 | export type SearchPredicate = (value: T, index?: number, obj?: ReadonlyArray) => boolean; 71 | 72 | /** Interface for IDE: autocomplete syntax and keywords */ 73 | export interface IStaticEnum extends EnumClass { 74 | 75 | new(): {enumName: string}; 76 | 77 | /** @returns all elements of enum */ 78 | values(): ReadonlyArray; 79 | 80 | /** 81 | * Get all names of enum. 82 | * It's shortcut for 'values().map(value => value.enumName)' 83 | * @returns all names of enum */ 84 | keys(): string[]; 85 | 86 | /** 87 | * Lookup enum item by id 88 | * @param id value for lookup or throw error 89 | * @throws enum item not found 90 | */ 91 | valueOf(id: string | number): T; 92 | 93 | /** 94 | * Lookup enum item by enum name 95 | * @param name enum name 96 | * @return item by enum name or throw error 97 | * @throws enum item not found 98 | */ 99 | valueByName(name: string): T; 100 | 101 | /** 102 | * Lookup enum item by id or search by predicate 103 | * @param idOrPredicate id or predicate 104 | * @return item ot `null` 105 | */ 106 | find(idOrPredicate: string | number | SearchPredicate): T | null; 107 | 108 | /** 109 | * Filter enum items by predicate 110 | * @param predicate function to filtering 111 | * @return items ot `[]` 112 | */ 113 | filter(predicate: SearchPredicate): ReadonlyArray; 114 | } 115 | 116 | /** Base class for enum type */ 117 | class Enumerable implements EnumItemType { 118 | // tslint:disable:variable-name 119 | // stub. need for type safety 120 | static readonly __store__ = {} as EnumStore; 121 | // Initialize inside @Enum decorator 122 | __enumName__ = ""; 123 | // tslint:enable:variable-name 124 | 125 | constructor() { 126 | } 127 | 128 | /** 129 | * Get all elements of enum 130 | * @return all elements of enum 131 | */ 132 | static values(): ReadonlyArray { 133 | return this.__store__.enumValues; 134 | } 135 | 136 | /** 137 | * Get all enum names 138 | * @return all enum names 139 | */ 140 | static keys(): string[] { 141 | return this.values().map(value => value.enumName); 142 | } 143 | 144 | /** 145 | * Lookup enum item by id 146 | * @param {string | number} id - value for lookup 147 | * @return enum item by id 148 | */ 149 | static valueOf(id: string | number): any { 150 | const value = this.__store__.enumMap[id]; 151 | if (!value) { 152 | throw new Error(`The element with ${id} identifier does not exist in the $ {clazz.name} enumeration`); 153 | } 154 | return value; 155 | } 156 | 157 | /** 158 | * Lookup enum item by enum name 159 | * @param name enum name 160 | * @return item by enum name 161 | */ 162 | static valueByName(name: string): any { 163 | const value = this.__store__.enumMapByName[name]; 164 | if (!value) { 165 | throw new Error(`The element with ${name} name does not exist in the ${this.__store__.name} enumeration`); 166 | } 167 | return value; 168 | } 169 | 170 | static find(idOrPredicate: string | number | SearchPredicate): any | null { 171 | if (typeof idOrPredicate === "number" || typeof idOrPredicate === "string") { 172 | return this.__store__.enumMap[idOrPredicate] || null; 173 | } 174 | return this.values().find(idOrPredicate) || null; 175 | } 176 | 177 | static filter(predicate: SearchPredicate): ReadonlyArray { 178 | return this.values().filter(predicate); 179 | } 180 | 181 | /** Get enum name */ 182 | get enumName(): string { 183 | return this.__enumName__; 184 | } 185 | 186 | /** Get enum id value or enum name */ 187 | toString(): string { 188 | const clazz = this.topClass; 189 | if (clazz.__store__.idPropertyName) { 190 | const self = this as any; 191 | return self[clazz.__store__.idPropertyName]; 192 | } 193 | return this.enumName; 194 | } 195 | 196 | private get topClass(): EnumClass { 197 | return this.constructor as any; 198 | } 199 | } 200 | 201 | /** 'Casting' method to make correct Enum Type */ 202 | export function EnumType(): IStaticEnum { 203 | return (> Enumerable); 204 | } 205 | 206 | /** Get Names Of Enums */ 207 | export type EnumConstNames = Exclude, "prototype">; 208 | 209 | type GetNames = { 210 | [K in keyof FromType]: 211 | FromType[K] extends KeepType ? 212 | Include extends true ? K : 213 | never : Include extends true ? 214 | never : K 215 | }[keyof FromType]; 216 | 217 | /** 218 | * Powerful tool to comfortable work with plain json struct like enum 219 | */ 220 | export class EnumTools { 221 | 222 | /** 223 | * Get all properties key as string 224 | * @param struct that to be scanned 225 | * @returns all properties name as string 226 | */ 227 | static keys(struct: T): string[] { 228 | return Object.keys(struct); 229 | } 230 | 231 | /** 232 | * Get all values 233 | * @param struct that to be scanned 234 | * @returns all properties name as string 235 | */ 236 | static values(struct: T): Array { 237 | return Object.keys(struct).map(key => struct[key as keyof T]); 238 | } 239 | 240 | /** 241 | * Reverse key and value 242 | * @param struct that to be scanned 243 | * @returns reversed struct 244 | */ 245 | static reverse(struct: T): {[key: string]: string} { 246 | const reversedStruct: {[key: string]: string} = {}; 247 | for (const key in struct) { 248 | reversedStruct[struct[key]] = key; 249 | } 250 | return reversedStruct; 251 | } 252 | 253 | /** 254 | * Get array of pair with `{key: string, value: string | number}` struct 255 | * @param struct that to be scanned 256 | * @returns array of pair with `{key: string, value: string | number}` struct 257 | */ 258 | static pairs(struct: T): Array<{key: string, value: string | number}> { 259 | const pairs = []; 260 | for (const key in struct) { 261 | pairs.push({ 262 | key: key, 263 | value: struct[key as keyof T] 264 | }); 265 | } 266 | return pairs; 267 | } 268 | 269 | /** 270 | * Creating class that extends from EnumType and has all static functionality 271 | * @param struct that to be scanned 272 | * @returns class that extends from EnumType and has all static functionality 273 | */ 274 | static toClass(struct: T) { 275 | // declare class of enum 276 | const KeyValueEnum = class InnerEnum extends EnumType() { 277 | constructor(readonly key: string, readonly value: string | number) { 278 | super(); 279 | } 280 | } 281 | // Dirty hack. Needs to be getting correct type of KeyValueEnum instance 282 | const IgnoredEnum = new KeyValueEnum("", ""); 283 | // add static constant 284 | for (const key of Object.keys(struct)) { 285 | ( KeyValueEnum)[key] = new KeyValueEnum(key, struct[key as keyof T]); 286 | } 287 | // wrap by Enum decorator 288 | const WrappedEnum = Enum("key")(KeyValueEnum); 289 | // complete correct type 290 | return > WrappedEnum; 291 | } 292 | } 293 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', or 'ESNEXT'. */ 4 | "module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */ 5 | "declaration": true, /* Generates corresponding '.d.ts' file. */ 6 | "sourceMap": true, 7 | "outDir": "./dist/commonjs", /* Redirect output structure to the directory. */ 8 | "rootDir": "./src/ts", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ 9 | "lib": [ 10 | "es6" 11 | ], 12 | 13 | /* Strict Type-Checking Options */ 14 | "strict": true, /* Enable all strict type-checking options. */ 15 | "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ 16 | "noImplicitAny": true, 17 | "noImplicitReturns": true, 18 | "experimentalDecorators": true, 19 | "emitDecoratorMetadata": true, 20 | "preserveConstEnums": true, 21 | 22 | "skipLibCheck": true, 23 | 24 | /* Module Resolution Options */ 25 | "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ 26 | "baseUrl": "./src/ts" /* Base directory to resolve non-absolute module names. */ 27 | }, 28 | "exclude": [ 29 | "./src/__tests__/*.test.ts", 30 | "./src/example/*.ts", 31 | "./node_modules" 32 | ] 33 | } -------------------------------------------------------------------------------- /tsconfig.test.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json" 3 | 4 | } -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "tslint:recommended", 3 | "rules": { 4 | /** По умолчанию public - модификатор доступа и его указывать не нужно */ 5 | "member-access": [true, "no-public"], 6 | 7 | /** Схема расположения объектов как для java классов */ 8 | "member-ordering": { 9 | "options": { 10 | "order": "fields-first" 11 | } 12 | }, 13 | 14 | /** Отключаем обязательное расположение ключей по алфавиту */ 15 | "object-literal-sort-keys": false, 16 | 17 | /** Максимальная длина строки - 160 символов. */ 18 | "max-line-length": { 19 | "options": [180] 20 | }, 21 | 22 | /** Разрешаем столько классов в одном файле, сколько требуется */ 23 | "max-classes-per-file": false, 24 | 25 | /** Явно типы указывать не нужно для переменных и для инициализируемых параметров */ 26 | "no-inferrable-types": true, 27 | 28 | /** Разрешаем не указывать фигурные скобки для простых стрелочных функций */ 29 | "arrow-parens": false, 30 | 31 | /** Разрешаем декларирование стрелочных функций */ 32 | "only-arrow-functions": ["allow-declarations"], 33 | 34 | /** Типизация объекта через , а не через 'AS'. */ 35 | "no-angle-bracket-type-assertion": false, 36 | 37 | /** Разрешаем пустые блоки, они используются в заглушках и инициализациях */ 38 | "no-empty": false, 39 | 40 | /** Отключаем обязательное указание запятой после последнего элемента */ 41 | "trailing-comma": false, 42 | 43 | /** Краткий стиль указания свойств в JSON объекте делаем необязательным */ 44 | "object-literal-shorthand": false, 45 | 46 | /** Разрешаем указывать и типы и интерфейсы */ 47 | "interface-over-type-literal": false, 48 | 49 | /** Файл не должен заканчиваться символом переноса строки */ 50 | "eofline": false, 51 | 52 | /* Разрешаем расширять базовые правила нашими */ 53 | "curly": true 54 | } 55 | } --------------------------------------------------------------------------------