├── .gitignore ├── package.json ├── LICENSE ├── index.ts ├── tsconfig.json ├── index.js └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "typescript-from-javascript", 3 | "version": "1.0.0", 4 | "description": "This is a project you can walk through commit-by-commit to see the transformation of JavaScript code to TypeScript.", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "build": "tsc", 9 | "build:w": "tsc -w" 10 | }, 11 | "keywords": [ 12 | "javascript", 13 | "typescript" 14 | ], 15 | "author": "Jeremy Likness (@JeremyLikness)", 16 | "license": "MIT", 17 | "devDependencies": { 18 | "typescript": "^3.1.1" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2018 Jeremy Likness 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /index.ts: -------------------------------------------------------------------------------- 1 | const delay = (fn: Function) => setTimeout(fn, 0); 2 | const delayDebug = (str: string) => delay(() => console.log(str)); 3 | 4 | delayDebug("\n\nDEBUG INFO:"); 5 | 6 | const getFnName = (fn: Function) => { 7 | let funcText = fn.toString(); 8 | let trimmed = funcText.substr('function '.length); 9 | let name = trimmed.substr(0, trimmed.indexOf('(')); 10 | return name.trim(); 11 | }; 12 | 13 | function logLifecycle(constructor:T) { 14 | let _new = new Proxy(constructor, { 15 | apply(target, _thisArg, argumentsList) { 16 | delay(() => console.log(`Constructed ${getFnName(constructor)} at ${new Date()}`)); 17 | return new target(...argumentsList); 18 | } 19 | }); 20 | return _new; 21 | } 22 | 23 | function debug(_target: Object, propertyKey: string, descriptor: TypedPropertyDescriptor) { 24 | let originalMethod = descriptor.value; // save a reference to the original method 25 | descriptor.value = function (...args: any[]) { 26 | delay(() => console.info(`The method args for ${propertyKey}() are: ${JSON.stringify(args)}`)); // pre 27 | var result = originalMethod.apply(this, args); // run and store the result 28 | delay(() => console.info(`The return value for ${propertyKey}() is: ${result}`)); // post 29 | return result; // return the result of the original method 30 | }; 31 | return descriptor; 32 | } 33 | 34 | type Phone = 'home' | 'mobile'; 35 | 36 | interface ICanPrint { 37 | print(): void; 38 | } 39 | 40 | const PrintRecursive = (parent: T, children?: (parent: T) => ICanPrint[]) => { 41 | delayDebug("Printing parent..."); 42 | parent.print(); 43 | if (children) { 44 | delayDebug("Printing children..."); 45 | const printJobs = children(parent); 46 | for (let idx = 0; idx < printJobs.length; idx += 1) { 47 | delayDebug(`Printing child at index ${idx}`); 48 | PrintRecursive(printJobs[idx]); 49 | } 50 | } 51 | } 52 | 53 | interface IAmContact { 54 | name: string; 55 | age: number; 56 | contactType: Phone; 57 | contactNumber: string; 58 | } 59 | 60 | const firstUpper = (inp: string) => `${inp.charAt(0).toLocaleUpperCase()}${inp.slice(1)}`; 61 | 62 | type ContactProperty = keyof IAmContact; 63 | 64 | const printProperty = (key: ContactProperty, contact: IAmContact) => { 65 | let value = contact[key]; 66 | if (typeof value === 'number') { 67 | console.log(`${firstUpper(key)}: ${value.toFixed(0)}`); 68 | } 69 | else { 70 | console.log(`${firstUpper(key)}: ${value}`); 71 | } 72 | } 73 | 74 | @logLifecycle 75 | class Contact implements IAmContact, ICanPrint { 76 | constructor( 77 | public name: string, 78 | public age: number, 79 | public contactType: Phone, 80 | public contactNumber: string) {} 81 | 82 | @debug 83 | print() { 84 | printProperty("name", this); 85 | printProperty("age", this); 86 | console.log(Contact.calculateYearBorn(this.age)); 87 | if (this.contactType === "mobile") { 88 | console.log("Cell phone:"); 89 | } 90 | else { 91 | console.log("Landline:"); 92 | } 93 | printProperty("contactNumber", this); 94 | } 95 | @debug 96 | public static calculateYearBorn(age: number): string { 97 | let thisYear = (new Date()).getFullYear(); 98 | return `Born around the year ${(thisYear - age).toFixed(0)}`; 99 | } 100 | } 101 | 102 | interface IAmContactList { 103 | contacts: Contact[]; 104 | } 105 | 106 | @logLifecycle 107 | class ContactList implements IAmContactList, ICanPrint { 108 | contacts: Contact[]; 109 | constructor(...contacts: Contact[]) { 110 | this.contacts = contacts; 111 | } 112 | @debug 113 | print () { 114 | console.log(`A rolodex with ${this.contacts.length} contacts`); 115 | } 116 | } 117 | 118 | const me = new Contact("Jeremy", 44.1, "mobile", "555-1212"); 119 | const myWife = new Contact("Doreen", 30, "home", "404-123-4567"); 120 | const rolodex = new ContactList(me, myWife); 121 | 122 | console.log("\n\nNormal print:"); 123 | PrintRecursive(rolodex); 124 | 125 | console.log("\n\nPrint with recursive:"); 126 | PrintRecursive(rolodex, rolodex => rolodex.contacts); 127 | 128 | type Predicate = (item: T) => boolean; 129 | 130 | const find = (list: T[], test: Predicate) => { 131 | for (let idx = 0; idx < list.length; idx += 1) { 132 | if (test(list[idx])) { 133 | return list[idx]; 134 | } 135 | } 136 | return null; 137 | } 138 | 139 | const found = find(rolodex.contacts, contact => contact.name === "Doreen"); 140 | 141 | if (found) { 142 | console.log("\n\nFound something:"); 143 | found.print(); 144 | } 145 | else { 146 | console.log("\n\nNot found!"); 147 | } 148 | 149 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Basic Options */ 4 | "target": "es2018", /* 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 | // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ 12 | // "sourceMap": true, /* Generates corresponding '.map' file. */ 13 | // "outFile": "./", /* Concatenate and emit output to single file. */ 14 | // "outDir": "./", /* Redirect output structure to the directory. */ 15 | // "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ 16 | // "composite": true, /* Enable project compilation */ 17 | // "removeComments": true, /* Do not emit comments to output. */ 18 | // "noEmit": true, /* Do not emit outputs. */ 19 | // "importHelpers": true, /* Import emit helpers from 'tslib'. */ 20 | // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ 21 | // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ 22 | 23 | /* Strict Type-Checking Options */ 24 | "strict": true, /* Enable all strict type-checking options. */ 25 | "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ 26 | "strictNullChecks": true, /* Enable strict null checks. */ 27 | "strictFunctionTypes": true, /* Enable strict checking of function types. */ 28 | "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ 29 | "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ 30 | "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ 31 | 32 | /* Additional Checks */ 33 | // "noUnusedLocals": true, /* Report errors on unused locals. */ 34 | // "noUnusedParameters": true, /* Report errors on unused parameters. */ 35 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ 36 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ 37 | 38 | /* Module Resolution Options */ 39 | // "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ 40 | // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ 41 | // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ 42 | // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ 43 | // "typeRoots": [], /* List of folders to include type definitions from. */ 44 | // "types": [], /* Type declaration files to be included in compilation. */ 45 | // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ 46 | "esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ 47 | // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ 48 | 49 | /* Source Map Options */ 50 | // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ 51 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 52 | // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ 53 | // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ 54 | 55 | /* Experimental Options */ 56 | "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ 57 | "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ 58 | } 59 | } -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) { 2 | var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d; 3 | if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc); 4 | else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r; 5 | return c > 3 && r && Object.defineProperty(target, key, r), r; 6 | }; 7 | var __metadata = (this && this.__metadata) || function (k, v) { 8 | if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v); 9 | }; 10 | var Contact_1; 11 | "use strict"; 12 | const delay = (fn) => setTimeout(fn, 0); 13 | const delayDebug = (str) => delay(() => console.log(str)); 14 | delayDebug("\n\nDEBUG INFO:"); 15 | const getFnName = (fn) => { 16 | let funcText = fn.toString(); 17 | let trimmed = funcText.substr('function '.length); 18 | let name = trimmed.substr(0, trimmed.indexOf('(')); 19 | return name.trim(); 20 | }; 21 | function logLifecycle(constructor) { 22 | let _new = new Proxy(constructor, { 23 | apply(target, _thisArg, argumentsList) { 24 | delay(() => console.log(`Constructed ${getFnName(constructor)} at ${new Date()}`)); 25 | return new target(...argumentsList); 26 | } 27 | }); 28 | return _new; 29 | } 30 | function debug(_target, propertyKey, descriptor) { 31 | let originalMethod = descriptor.value; // save a reference to the original method 32 | descriptor.value = function (...args) { 33 | delay(() => console.info(`The method args for ${propertyKey}() are: ${JSON.stringify(args)}`)); // pre 34 | var result = originalMethod.apply(this, args); // run and store the result 35 | delay(() => console.info(`The return value for ${propertyKey}() is: ${result}`)); // post 36 | return result; // return the result of the original method 37 | }; 38 | return descriptor; 39 | } 40 | const PrintRecursive = (parent, children) => { 41 | delayDebug("Printing parent..."); 42 | parent.print(); 43 | if (children) { 44 | delayDebug("Printing children..."); 45 | const printJobs = children(parent); 46 | for (let idx = 0; idx < printJobs.length; idx += 1) { 47 | delayDebug(`Printing child at index ${idx}`); 48 | PrintRecursive(printJobs[idx]); 49 | } 50 | } 51 | }; 52 | const firstUpper = (inp) => `${inp.charAt(0).toLocaleUpperCase()}${inp.slice(1)}`; 53 | const printProperty = (key, contact) => { 54 | let value = contact[key]; 55 | if (typeof value === 'number') { 56 | console.log(`${firstUpper(key)}: ${value.toFixed(0)}`); 57 | } 58 | else { 59 | console.log(`${firstUpper(key)}: ${value}`); 60 | } 61 | }; 62 | let Contact = Contact_1 = class Contact { 63 | constructor(name, age, contactType, contactNumber) { 64 | this.name = name; 65 | this.age = age; 66 | this.contactType = contactType; 67 | this.contactNumber = contactNumber; 68 | } 69 | print() { 70 | printProperty("name", this); 71 | printProperty("age", this); 72 | console.log(Contact_1.calculateYearBorn(this.age)); 73 | if (this.contactType === "mobile") { 74 | console.log("Cell phone:"); 75 | } 76 | else { 77 | console.log("Landline:"); 78 | } 79 | printProperty("contactNumber", this); 80 | } 81 | static calculateYearBorn(age) { 82 | let thisYear = (new Date()).getFullYear(); 83 | return `Born around the year ${(thisYear - age).toFixed(0)}`; 84 | } 85 | }; 86 | __decorate([ 87 | debug, 88 | __metadata("design:type", Function), 89 | __metadata("design:paramtypes", []), 90 | __metadata("design:returntype", void 0) 91 | ], Contact.prototype, "print", null); 92 | __decorate([ 93 | debug, 94 | __metadata("design:type", Function), 95 | __metadata("design:paramtypes", [Number]), 96 | __metadata("design:returntype", String) 97 | ], Contact, "calculateYearBorn", null); 98 | Contact = Contact_1 = __decorate([ 99 | logLifecycle, 100 | __metadata("design:paramtypes", [String, Number, String, String]) 101 | ], Contact); 102 | let ContactList = class ContactList { 103 | constructor(...contacts) { 104 | this.contacts = contacts; 105 | } 106 | print() { 107 | console.log(`A rolodex with ${this.contacts.length} contacts`); 108 | } 109 | }; 110 | __decorate([ 111 | debug, 112 | __metadata("design:type", Function), 113 | __metadata("design:paramtypes", []), 114 | __metadata("design:returntype", void 0) 115 | ], ContactList.prototype, "print", null); 116 | ContactList = __decorate([ 117 | logLifecycle, 118 | __metadata("design:paramtypes", [Contact]) 119 | ], ContactList); 120 | const me = new Contact("Jeremy", 44.1, "mobile", "555-1212"); 121 | const myWife = new Contact("Doreen", 30, "home", "404-123-4567"); 122 | const rolodex = new ContactList(me, myWife); 123 | console.log("\n\nNormal print:"); 124 | PrintRecursive(rolodex); 125 | console.log("\n\nPrint with recursive:"); 126 | PrintRecursive(rolodex, rolodex => rolodex.contacts); 127 | const find = (list, test) => { 128 | for (let idx = 0; idx < list.length; idx += 1) { 129 | if (test(list[idx])) { 130 | return list[idx]; 131 | } 132 | } 133 | return null; 134 | }; 135 | const found = find(rolodex.contacts, contact => contact.name === "Doreen"); 136 | if (found) { 137 | console.log("\n\nFound something:"); 138 | found.print(); 139 | } 140 | else { 141 | console.log("\n\nNot found!"); 142 | } 143 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # TypeScript from JavaScript 2 | 3 | This is a project you can walk through commit-by-commit to see the transformation of JavaScript code to TypeScript. 4 | 5 | [@JeremyLikness](https://twitter.com/JeremyLikness) 6 | 7 | Background notes: 8 | 9 | * 1995: Mocha becomes LiveScript, written in 10 days 10 | * 1996: JavaScript lives! 11 | * 1997: ECMAScript 1 sets standards 12 | * 1999: ECMAScript 3 13 | * 2005: jQuery "Normalize the DOM" 14 | * 2009: Node.js, ECMAScript 5 15 | * 2012: TypeScript (and there was much rejoicing) 16 | * 2015: ECMAScript 2015 ("Harmony") 17 | 18 | ## Intro 19 | 20 | `git checkout d65aed6` 21 | 22 | Currently, there is a small JavaScript "app" that you can run with the command: 23 | 24 | `node index.js` 25 | 26 | There are some defects and if you browse to JavaScript in your IDE, you may or may not get appropriate hints about what's wrong. 27 | 28 | ## Transform to TypeScript 29 | 30 | `git checkout 08f09e3` 31 | 32 | JavaScript is valid TypeScript (with a few exceptions) so we'll start by setting up TypeScript. 33 | 34 | `npm install --save-dev typescript` 35 | 36 | Add two build scripts to the `package.json` file for compiling and compiling on a watch: 37 | 38 | ```javascript 39 | "scripts": { 40 | "build": "tsc", 41 | "build:w": "tsc -w" 42 | } 43 | ``` 44 | 45 | To create a configuration file that TypeScript uses to compile source to JavaScript, run: 46 | 47 | `./node_modules/typescript/bin/tsc --init` 48 | 49 | Finally, rename `index.js` to `index.ts`. 50 | 51 | > Ouch! There are a lot of errors. We'll address these in the next commit. 52 | 53 | ## Turn Off Strict Typing and Add a Spread Operator 54 | 55 | `git checkout 1b9c8b1` 56 | 57 | It's been said all valid JavaScript is valid TypeScript. That is only partly true. TypeScript enables configuration options and by default expects "strict typing" without implicitly allowing a variable to be any type. For now, turn off strict typing by setting it to false in the `tsconfig.json`: 58 | 59 | ```javascript 60 | { strict: "false" } 61 | ``` 62 | 63 | Next, you will find our first bug. `ContactList` is expecting an array but what's passed is technically two parameters. You *could* change the constructor call to pass an array like this: 64 | 65 | ```javascript 66 | new ContactList([me, myWife]) 67 | ``` 68 | 69 | A more flexible solution is to accept 1-to-many items passed in by using the spread operator. Change the function declaration for the `ContactList` to this: 70 | 71 | ```javascript 72 | function ContactList(...contacts) { 73 | ``` 74 | 75 | Now compile and run the code. 76 | 77 | ```bash 78 | npm run-script build 79 | node index.js 80 | ``` 81 | 82 | That's better. There are still some bugs though. The contact type is printing as landline for both contacts, the phone number isn't showing, and the debug information prints "2" each time. 83 | 84 | Before this is addressed, take a look at the compiled `index.js`. Compare to the source TypeScript. Notice it's only slightly different. Although modern JavaScript supports the the "spread operator", it's not supported in the older JavaScript version so TypeScript builds code to make it compatible for you! Later on you'll see the modern version of the compiled JavaScript. For now, let's refactor our function constructors to "real" classes. 85 | 86 | ## Refactor to "real" classes 87 | 88 | `git checkout c39795a` 89 | 90 | TypeScript supports a class definition. For older JavaScript, it will generate the appropriate code to "wrap" the class behavior. For newer JavaScript it will generate the native class keyword. The `Contact` and `ContactList` entities are refactored to a class. The constructor is the same as the original function call, with the difference that the parameters are declared `public` and given types. This implicitly creates the properties on the class and moves the constructor parameters to the properties. The generated JavaScript isn't available yet, because a bug has already been identified. 91 | 92 | ## Fix Two Bugs 93 | 94 | `git checkout b3974e5` 95 | 96 | The first bug is a naming issue. The class defines a property called `phoneNumber` but the print method references `contactNumber`. Change the property to `contactNumber` to be consistent with `contactType`. 97 | 98 | Next, create a type called `Phone` that allows a value of either `mobile` or `home`. Change the `contactType` to be of type `Phone` and fix several more defects. Now when you compile and run it should print as expected, except for the debug information. 99 | 100 | ## Lambdas, let, and string interpolation 101 | 102 | `git checkout 3be05fe` 103 | 104 | Lambda expressions help by automatically preserving the reference to `this` and pass it to subsequent nested expressions so there are not unexpected side effects. The `let` keyword indicates a variable is indeed local, and TypeScript will generate additional code for it to behave properly in closure situations to preserve the intended scope. Finally, string interpolation makes it easier to read concatenated strings in the source. It is leveraged as a native feature in modern JavaScript and turned back to "string addition" for older JavaScript. 105 | 106 | *** (Add a reference to `this.contacts` for more insight into the lambdas). 107 | 108 | ## Find 109 | 110 | `git checkout 788d493` 111 | 112 | Add a "find" function to search the contacts and return an example. The initial implementation fails to find anything. Paste this code, compile and run it: 113 | 114 | ```javascript 115 | const find = (list, test) => { 116 | for (let idx = 0; idx < list.length; idx += 1) { 117 | if (test(list[idx])) { 118 | return list[idx]; 119 | } 120 | } 121 | return null; 122 | } 123 | 124 | const found = find(rolodex, contact => contact.Name === "Doreen"); 125 | 126 | if (found) { 127 | console.log("\n\nFound something:"); 128 | found.print(); 129 | } 130 | else { 131 | console.log("\n\nNot found!"); 132 | } 133 | ``` 134 | 135 | ## The Power of Generics 136 | 137 | `git checkout 14769c5` 138 | 139 | Generics help by creating a template for a type, then providing hints and type-checking for that type. To see this in action, refactor the `find` function to use generics. Immediately a bug is revealed. 140 | 141 | ```TypeScript 142 | const find = (list: T[], test: (item: T) => boolean) => { 143 | for (let idx = 0; idx < list.length; idx += 1) { 144 | if (test(list[idx])) { 145 | return list[idx]; 146 | } 147 | } 148 | return null; 149 | } 150 | ``` 151 | 152 | ## Fix it 153 | 154 | `git checkout b1aae76` 155 | 156 | The generic function revealed another defect. Fixing that reveals yet another bug that can be fixed. Compile and run and the search should work. 157 | 158 | ## Simplify it 159 | 160 | `git checkout 9ceda1c` 161 | 162 | Create a type called "predicate" to simplify the definition of find. Note this doesn't change the compiled JavaScript at all. 163 | 164 | ## Interfaces and optional parameters 165 | 166 | `git checkout c27d282` 167 | 168 | Interfaces help describe types. Refactor to use interfaces. Add a `PrintRecursive` helper function with an optional parameter. Also make debugging easier with a delayDebug function. 169 | 170 | ```TypeScript 171 | const PrintRecursive = (parent: T, children?: (parent: T) => ICanPrint[]) => { 172 | delayDebug("Printing parent..."); 173 | parent.print(); 174 | if (children) { 175 | delayDebug("Printing children..."); 176 | const printJobs = children(parent); 177 | for (let idx = 0; idx < printJobs.length; idx += 1) { 178 | delayDebug(`Printing child at index ${idx}`); 179 | PrintRecursive(printJobs[idx]); 180 | } 181 | } 182 | } 183 | ``` 184 | 185 | ## Formatting 186 | 187 | `git checkout 7861a91` 188 | 189 | Add a simple function to help with printing contact info so it can print in label: value fashion. 190 | 191 | ```TypeScript 192 | const firstUpper = (inp: string) => `${inp.charAt(0).toLocaleUpperCase()}${inp.slice(1)}`; 193 | 194 | const printProperty = (key, contact: IAmContact) => { 195 | console.log(`${firstUpper(key)}: ${contact[key]}`); 196 | } 197 | 198 | // on contact class print() function 199 | 200 | print() { 201 | printProperty("Name", this); 202 | printProperty("age", this); 203 | if (this.contactType === "mobile") { 204 | console.log("Cell phone:"); 205 | } 206 | else { 207 | console.log("Landline:"); 208 | } 209 | printProperty("contactNumber", this); 210 | } 211 | ``` 212 | 213 | A bug was purposefully introduced: name shows `undefined`. The next iteration will catch and fix that. 214 | 215 | ## Key types 216 | 217 | `git checkout df156fc` 218 | 219 | Create a type named `ContactProperty` that uses `keyof` to fix the `printProperty` function. The defect is immediately apparent and can be fixed. 220 | 221 | ## Type guards 222 | 223 | `git checkout 733725b` 224 | 225 | Type guards allow IntelliSense to operate on a code block based on logic that enforces a type within the block. Update the `printProperty` function to check for a numeric type and print it fixed (without the decimal). 226 | 227 | ## Bonus: Strict typing and decorators 228 | 229 | `git checkout df156fc` 230 | 231 | Decorators make it easier to write a behavior once and apply it multiple times. The following code blocks include a helper method to grab the name of a function, a decorate for a constructor, and a decorator to "debug" a method call. Apply them liberally to classes and methods to see the results. Just place `@logLifeCycle` on a class definition or `@debug` before a method definition. The support for experimental decorators and metadata must be set to `true` in `tsconfig.json`. 232 | 233 | A static method is added to calculate approximate year born and called from the `print` method to demonstrate interception of arguments and return values. 234 | 235 | Turn on strict typing and refactor a few methods that don't have explicit types. 236 | 237 | ```TypeScript 238 | const getFnName = (fn: Function) => { 239 | let funcText = fn.toString(); 240 | let trimmed = funcText.substr('function '.length); 241 | let name = trimmed.substr(0, trimmed.indexOf('(')); 242 | return name.trim(); 243 | }; 244 | 245 | function logLifecycle(constructor:T) { 246 | class newCtor extends constructor { 247 | constructor(...args: any[]) { 248 | super(args); 249 | delay(() => console.log(`Constructed ${getFnName(constructor)} at ${new Date()}`)); 250 | return constructor.apply(this, args); 251 | } 252 | } 253 | newCtor.prototype = constructor.prototype; 254 | return newCtor; 255 | } 256 | 257 | function debug(_target: Object, propertyKey: string, descriptor: TypedPropertyDescriptor) { 258 | let originalMethod = descriptor.value; // save a reference to the original method 259 | descriptor.value = function (...args: any[]) { 260 | delay(() => console.info(`The method args for ${propertyKey}() are: ${JSON.stringify(args)}`)); // pre 261 | var result = originalMethod.apply(this, args); // run and store the result 262 | delay(() => console.info(`The return value for ${propertyKey}() is: ${result}`)); // post 263 | return result; // return the result of the original method 264 | }; 265 | return descriptor; 266 | } 267 | ``` 268 | 269 | ## PostScript 270 | 271 | This version illustrates output using a more modern JavaScript version. The target was changed in `tsconfig.json` to `es2018`. Note that the constructor decorate had to also be modified to use the new `Proxy` object. 272 | --------------------------------------------------------------------------------