├── .gitignore ├── .npmignore ├── bash └── update-all.sh ├── imgs ├── 2018-05-30 21_49_13-final.ts - typescript-mix - Visual Studio Code.png └── 2018-05-30 21_32_46-Preview README.md - typescript-mix - Visual Studio Code.png ├── dist ├── index.d.ts ├── index.js.map └── index.js ├── package.json ├── src └── index.ts ├── tsconfig.json └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | test/ 3 | spec/ 4 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | src/ 2 | test/ 3 | spec/ 4 | node_modules 5 | imgs/ 6 | tsconfig.json -------------------------------------------------------------------------------- /bash/update-all.sh: -------------------------------------------------------------------------------- 1 | tsc 2 | git-commit.sh $1 3 | git push origin master 4 | npm publish -------------------------------------------------------------------------------- /imgs/2018-05-30 21_49_13-final.ts - typescript-mix - Visual Studio Code.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/michaelolof/typescript-mix/HEAD/imgs/2018-05-30 21_49_13-final.ts - typescript-mix - Visual Studio Code.png -------------------------------------------------------------------------------- /imgs/2018-05-30 21_32_46-Preview README.md - typescript-mix - Visual Studio Code.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/michaelolof/typescript-mix/HEAD/imgs/2018-05-30 21_32_46-Preview README.md - typescript-mix - Visual Studio Code.png -------------------------------------------------------------------------------- /dist/index.d.ts: -------------------------------------------------------------------------------- 1 | export declare type Constructor = new (...args: any[]) => T; 2 | export declare type Mixin = Constructor | object; 3 | /** 4 | * Takes a list of classes or object literals and adds their methods 5 | * to the class calling it. 6 | */ 7 | export declare function use(...options: Mixin[]): (target: any, propertyKey: string) => void; 8 | /** 9 | * Takes a method as a parameter and add it to the class calling it. 10 | */ 11 | export declare function delegate(method: (...args: any[]) => any): (target: any, propertyKey: string) => void; 12 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "typescript-mix", 3 | "version": "3.1.3", 4 | "description": "A tweaked implementation of TypeScript's default applyMixins(...) idea using ES7 decorators", 5 | "main": "dist/index.js", 6 | "typings": "dist/index.d.ts", 7 | "scripts": { 8 | "test": "cross-env NODE_ENV=development mocha -r C:\\Users\\Michael\\AppData\\Roaming\\npm\\node_modules\\ts-node\\register test/**/*.test.ts --reporter=\"min\" --watch --watch-extensions ts" 9 | }, 10 | "keywords": [ 11 | "traits in typescript", 12 | "use in typescript", 13 | "TypeScript Mixins", 14 | "Mixins", 15 | "applyMixins", 16 | "mix", 17 | "multiple inheritance in TypeScript" 18 | ], 19 | "author": "michaelolof", 20 | "repository": "git+https://github.com/michaelolof/typescript-mix.git", 21 | "license": "ISC", 22 | "dependencies": {}, 23 | "devDependencies": { 24 | "@types/chai": "^4.1.3", 25 | "@types/mocha": "^5.2.0", 26 | "chai": "^4.1.2", 27 | "cross-env": "^5.1.6", 28 | "mocha": "^5.1.1" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /dist/index.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";;AAGA,aAAa,MAAwB,EAAE,MAAoB;IACzD,IAAM,UAAU,GAAG,MAAM,CAAC,mBAAmB,CAAE,MAAM,CAAC,SAAS,CAAE,CAAC;IAClE,KAAkB,UAAM,EAAN,iBAAM,EAAN,oBAAM,EAAN,IAAM;QAAnB,IAAI,KAAK,eAAA;QACZ,IAAM,aAAa,GAAG,WAAW,CAAC,UAAU,EAAE,KAAK,CAAC,CAAC;QACrD,MAAM,CAAC,gBAAgB,CAAE,MAAM,CAAC,SAAS,EAAE,aAAa,CAAE,CAAC;KAC5D;AACH,CAAC;AAED;;GAEG;AACH,qBAAqB,UAAmB,EAAE,KAAiB;IACzD,IAAI,WAAW,GAAyB,EAAE,CAAC;IAC3C,QAAQ,OAAO,KAAK,EAAE;QACpB,KAAK,QAAQ;YACX,WAAW,GAAG,WAAW,CAAC,KAAK,CAAC,CAAC;YACjC,MAAM;QACR,KAAK,UAAU;YACb,WAAW,GAAG,WAAW,CAAE,KAA0B,CAAC,SAAS,CAAC,CAAC;YACjE,MAAM;KACT;IACD,OAAO,WAAW,CAAC;IAEnB,qBAAqB,GAAU;QAC7B,IAAM,GAAG,GAAyB,EAAE,CAAC;QACrC,MAAM,CAAC,mBAAmB,CAAE,GAAG,CAAE,CAAC,GAAG,CAAE,UAAA,GAAG;YACxC,IAAI,UAAU,CAAC,OAAO,CAAE,GAAG,CAAE,GAAG,CAAC,EAAG;gBAClC,IAAM,UAAU,GAAG,MAAM,CAAC,wBAAwB,CAAE,GAAG,EAAE,GAAG,CAAE,CAAC;gBAC/D,IAAI,UAAU,KAAK,SAAS;oBAAG,OAAM;gBACrC,IAAI,UAAU,CAAC,GAAG,IAAI,UAAU,CAAC,GAAG,EAAG;oBACrC,GAAG,CAAE,GAAG,CAAE,GAAG,UAAU,CAAC;iBACzB;qBAED,IAAK,OAAO,UAAU,CAAC,KAAK,KAAK,UAAU,EAAG;oBAC5C,GAAG,CAAE,GAAG,CAAE,GAAG,UAAU,CAAC;iBACzB;aACF;QACH,CAAC,CAAC,CAAA;QACF,OAAO,GAAG,CAAC;IACb,CAAC;AAEH,CAAC;AAED;;;GAGG;AACH;IAAoB,iBAAwB;SAAxB,UAAwB,EAAxB,qBAAwB,EAAxB,IAAwB;QAAxB,4BAAwB;;IAC1C,OAAO,UAAU,MAAW,EAAE,WAAmB;QAC/C,GAAG,CAAC,MAAM,CAAC,WAAW,EAAE,OAAO,CAAC,OAAO,EAAE,CAAC,CAAC;IAC7C,CAAC,CAAA;AACH,CAAC;AAJD,kBAIC;AAED;;GAEG;AACH,kBAAyB,MAA+B;IACtD,OAAO,UAAU,MAAW,EAAE,WAAmB;QAC/C,MAAM,CAAC,WAAW,CAAC,SAAS,CAAC,WAAW,CAAC,GAAG,MAAM,CAAC;IACrD,CAAC,CAAA;AACH,CAAC;AAJD,4BAIC"} -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export type Constructor = new (...args: any[]) => T; 2 | export type Mixin = Constructor | object; 3 | 4 | function mix(client: Constructor, mixins: Mixin[]) { 5 | const clientKeys = Object.getOwnPropertyNames( client.prototype ); 6 | for (let mixin of mixins) { 7 | const mixinMixables = getMixables(clientKeys, mixin); 8 | Object.defineProperties( client.prototype, mixinMixables ); 9 | } 10 | } 11 | 12 | /** 13 | * Returns a map of mixables. That is things that can be mixed in 14 | */ 15 | function getMixables(clientKeys:string[], mixin: Mixin) { 16 | let descriptors:PropertyDescriptorMap = {}; 17 | switch (typeof mixin) { 18 | case "object": 19 | descriptors = getMixables(mixin); 20 | break; 21 | case "function": 22 | descriptors = getMixables((mixin as Constructor).prototype); 23 | break; 24 | } 25 | return descriptors; 26 | 27 | function getMixables(obj:object):PropertyDescriptorMap { 28 | const map:PropertyDescriptorMap = {}; 29 | Object.getOwnPropertyNames( obj ).map( key => { 30 | if( clientKeys.indexOf( key ) < 0 ) { 31 | const descriptor = Object.getOwnPropertyDescriptor( obj, key ); 32 | if( descriptor === undefined ) return 33 | if( descriptor.get || descriptor.set ) { 34 | map[ key ] = descriptor; 35 | } 36 | else 37 | if ( typeof descriptor.value === "function" ) { 38 | map[ key ] = descriptor; 39 | } 40 | } 41 | }) 42 | return map; 43 | } 44 | 45 | } 46 | 47 | /** 48 | * Takes a list of classes or object literals and adds their methods 49 | * to the class calling it. 50 | */ 51 | export function use(...options: Mixin[]) { 52 | return function (target: any, propertyKey: string) { 53 | mix(target.constructor, options.reverse()); 54 | } 55 | } 56 | 57 | /** 58 | * Takes a method as a parameter and add it to the class calling it. 59 | */ 60 | export function delegate(method: (...args: any[]) => any) { 61 | return function (target: any, propertyKey: string) { 62 | target.constructor.prototype[propertyKey] = method; 63 | } 64 | } -------------------------------------------------------------------------------- /dist/index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | function mix(client, mixins) { 4 | var clientKeys = Object.getOwnPropertyNames(client.prototype); 5 | for (var _i = 0, mixins_1 = mixins; _i < mixins_1.length; _i++) { 6 | var mixin = mixins_1[_i]; 7 | var mixinMixables = getMixables(clientKeys, mixin); 8 | Object.defineProperties(client.prototype, mixinMixables); 9 | } 10 | } 11 | /** 12 | * Returns a map of mixables. That is things that can be mixed in 13 | */ 14 | function getMixables(clientKeys, mixin) { 15 | var descriptors = {}; 16 | switch (typeof mixin) { 17 | case "object": 18 | descriptors = getMixables(mixin); 19 | break; 20 | case "function": 21 | descriptors = getMixables(mixin.prototype); 22 | break; 23 | } 24 | return descriptors; 25 | function getMixables(obj) { 26 | var map = {}; 27 | Object.getOwnPropertyNames(obj).map(function (key) { 28 | if (clientKeys.indexOf(key) < 0) { 29 | var descriptor = Object.getOwnPropertyDescriptor(obj, key); 30 | if (descriptor === undefined) 31 | return; 32 | if (descriptor.get || descriptor.set) { 33 | map[key] = descriptor; 34 | } 35 | else if (typeof descriptor.value === "function") { 36 | map[key] = descriptor; 37 | } 38 | } 39 | }); 40 | return map; 41 | } 42 | } 43 | /** 44 | * Takes a list of classes or object literals and adds their methods 45 | * to the class calling it. 46 | */ 47 | function use() { 48 | var options = []; 49 | for (var _i = 0; _i < arguments.length; _i++) { 50 | options[_i] = arguments[_i]; 51 | } 52 | return function (target, propertyKey) { 53 | mix(target.constructor, options.reverse()); 54 | }; 55 | } 56 | exports.use = use; 57 | /** 58 | * Takes a method as a parameter and add it to the class calling it. 59 | */ 60 | function delegate(method) { 61 | return function (target, propertyKey) { 62 | target.constructor.prototype[propertyKey] = method; 63 | }; 64 | } 65 | exports.delegate = delegate; 66 | //# sourceMappingURL=index.js.map -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Basic Options */ 4 | "target": "es5", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017','ES2018' or 'ESNEXT'. */ 5 | "module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */ 6 | // "lib": [], /* Specify library files to be included in the compilation. */ 7 | // "allowJs": true, /* Allow javascript files to be compiled. */ 8 | // "checkJs": true, /* Report errors in .js files. */ 9 | // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ 10 | "declaration": true, /* Generates corresponding '.d.ts' file. */ 11 | "sourceMap": true, /* Generates corresponding '.map' file. */ 12 | // "outFile": "./", /* Concatenate and emit output to single file. */ 13 | "outDir": "./dist", /* Redirect output structure to the directory. */ 14 | "rootDir": "./src", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ 15 | // "removeComments": true, /* Do not emit comments to output. */ 16 | // "noEmit": true, /* Do not emit outputs. */ 17 | // "importHelpers": true, /* Import emit helpers from 'tslib'. */ 18 | // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ 19 | // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ 20 | 21 | /* Strict Type-Checking Options */ 22 | "strict": true, /* Enable all strict type-checking options. */ 23 | // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ 24 | // "strictNullChecks": true, /* Enable strict null checks. */ 25 | // "strictFunctionTypes": true, /* Enable strict checking of function types. */ 26 | // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ 27 | // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ 28 | // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ 29 | 30 | /* Additional Checks */ 31 | // "noUnusedLocals": true, /* Report errors on unused locals. */ 32 | // "noUnusedParameters": true, /* Report errors on unused parameters. */ 33 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ 34 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ 35 | 36 | /* Module Resolution Options */ 37 | // "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ 38 | // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ 39 | // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ 40 | // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ 41 | // "typeRoots": [], /* List of folders to include type definitions from. */ 42 | // "types": [], /* Type declaration files to be included in compilation. */ 43 | // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ 44 | "esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ 45 | // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ 46 | 47 | /* Source Map Options */ 48 | // "sourceRoot": "./", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ 49 | // "mapRoot": "./", /* Specify the location where debugger should locate map files instead of generated locations. */ 50 | // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ 51 | // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ 52 | 53 | /* Experimental Options */ 54 | "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ 55 | "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ 56 | }, 57 | "include": [ 58 | "./src" 59 | ] 60 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # TypeScript Mix 2 | 3 | A tweaked implementation of TypeScript's default applyMixins(...) idea using ES7 decorators. 4 | 5 | ## Breaking Changes from Version 3.0.0 upwards 6 | * New decorator @delegate introduced 7 | * Changes made in how multiple mixins implementing the same method are mixed 8 | 9 | See [Breaking Changes Explained](#breaking-changes-explained) 10 | 11 | ## Dependencies 12 | * TypeScript 13 | * ES7 Decorators 14 | 15 | 16 | ## Installation 17 | ``` 18 | npm install --save typescript-mix 19 | ``` 20 | 21 | ## Features 22 | * Properties in a mixin are not mixed into the client. They are ignored. See [TypeScript Mix — Yet Another Mixin Library](https://medium.com/@michaelolof/typescript-mix-yet-another-mixin-library-29c7a349b47d) for a detailed explanation on why. 23 | * Classes and Object Literals can be used as mixins. 24 | 25 | 26 | ## Goals 27 | 28 | * Ensure programming to an interface and not just only multiple implementations. 29 | 30 | * Create simple mixins that implement that interface 31 | 32 | * Provide an intuitive and readable way of using such mixins in a concrete class. 33 | 34 | 35 | 36 | ## Why I wrote yet another Mixin Library. 37 | 38 | The mixin pattern is somewhat a popular pattern amongst JavaScript/TypeScript devs as it gives the power of "mixin in" additional functionality to a class. The official way of using mixins as declared by Microsoft in TypeScript can be really verbose to downright unreadable. 39 | 40 | 41 | ## How to use 42 | 43 | ### The 'use' decorator 44 | 45 | #### Program to an interface. 46 | 47 | ``` 48 | interface Buyer { 49 | price: number 50 | buy(): void 51 | negotiate(): void 52 | } 53 | ``` 54 | 55 | Create a reusable implementation for that interface and that interface alone (Mixin) 56 | 57 | ``` 58 | const Buyer: Buyer = { 59 | price: undefined, 60 | buy() { 61 | console.log("buying items at #", this.price ); 62 | }, 63 | negotitate(price: number) { 64 | console.log("currently negotiating..."); 65 | this.price = price; 66 | }, 67 | } 68 | ``` 69 | 70 | Define another mixin this time using a Class declaration. 71 | ``` 72 | class Transportable { 73 | distance:number; 74 | transport() { 75 | console.log(`moved ${this.distance}km.`); 76 | } 77 | } 78 | ``` 79 | 80 | 81 | Define a concrete class that utilizes the defined mixins. 82 | 83 | ``` 84 | import use from "typescript-mix"; 85 | 86 | class Shopperholic { 87 | @use( Buyer, Transportable ) this 88 | 89 | price = 2000; 90 | distance = 140; 91 | } 92 | 93 | const shopper = new Shopperholic(); 94 | shopper.buy() // buying items at #2000 95 | shopper.negotiate(500) // currently negotiating... 96 | shopper.price // 500 97 | shopper.transport() // moved 140km 98 | ``` 99 | 100 | #### What about intellisense support? 101 | We trick typescript by using the inbuilt interface inheritance and declaration merging ability. 102 | ``` 103 | interface Shopperholic extends Buyer, Transportable {} 104 | 105 | class Shopperholic { 106 | @use( Buyer, Transportable ) this 107 | 108 | price = 2000; 109 | distance = 140; 110 | } 111 | ``` 112 | 113 | ### The 'delegate' decorator 114 | The delegate decorator is useful when we want specific functionality mixed into the client. 115 | ``` 116 | class OtherClass { 117 | simpleMethod() { 118 | console.log("This method has no dependencies"); 119 | } 120 | } 121 | 122 | function workItOut() { 123 | console.log("I am working it out.") 124 | } 125 | 126 | class MyClass { 127 | @delegate( OtherClass.prototype.simpleMethod ) 128 | simpleMethod:() => void 129 | 130 | @delegate( workItOut ) workItOut:() => void 131 | } 132 | 133 | const cls = new MyClass(); 134 | cls.simpleMethod() // This method has no dependencies 135 | cls.workItOut() // I am working it out 136 | ``` 137 | 138 | 139 | ## Things to note about this library? 140 | * using the 'use' decorator mutates the class prototype. This doesn't depend on inheritance (But if you use mixins correctly, you should be fine) 141 | 142 | * mixins don't override already declared methods or fields in the concrete class using them. 143 | 144 | * Mixins take precedence over a super class. i.e. they would override any field or method from a super class with the same name. 145 | 146 | * instance variables/fields/properties can be declared or even initialized in your mixins. This is necessary if you're defining methods that depend on object or class properties but these properties won't be mixed-in to the base class so you have to redefine those properties in the base class using the mixin. 147 | 148 | 149 | ## Advantages 150 | * The Library is non-obtrusive. Inheritance still works, (multiple inheritance still works ('Real Mixins Style')). 151 | 152 | ## Breaking Changes Explained 153 | ### The delegate decorator 154 | The addition of the delegate decorator now means module is imported as: 155 | ``` 156 | import { use, delegate } from "typescript-mix" 157 | ``` 158 | 159 | ### Multiple Mixins with the same method. 160 | Consider the following piece of code. 161 | ![alt text](https://github.com/michaelolof/typescript-mix/blob/master/imgs/2018-05-30%2021_32_46-Preview%20README.md%20-%20typescript-mix%20-%20Visual%20Studio%20Code.png?raw=true) 162 | 163 | Cleint One uses two mixins that contain the same method mixIt(). How do we resolve this? Which method gets picked?. 164 | One advantage of extending interfaces as we've defined above is that we're essentially telling TypeScript to mix-in the two mixin interfaces into the ClientOne interface. So how does TypeScript resolve this? 165 | 166 | ![alt text](https://github.com/michaelolof/typescript-mix/blob/master/imgs/2018-05-30%2021_49_13-final.ts%20-%20typescript-mix%20-%20Visual%20Studio%20Code.png?raw=true) 167 | 168 | Notice that TypeScript's intellisense calls MixinOne.mixIt() method. Therefore to be consistent with TypeScript and avoid confusion the '@use' decorator also implements MixinOne.mixIt() method. --------------------------------------------------------------------------------