├── .gitignore ├── LICENSE ├── README.md ├── TicTacToe_JS ├── .babelrc ├── css │ └── style.css ├── index.html ├── package-lock.json ├── package.json ├── src │ ├── app.jsx │ ├── board.jsx │ ├── constants.js │ ├── gameStateBar.jsx │ └── restartBtn.jsx └── webpack.config.js ├── TicTacToe_TS ├── css │ └── style.css ├── index.html ├── package-lock.json ├── package.json ├── src │ ├── app.tsx │ ├── board.tsx │ ├── constants.ts │ ├── gameStateBar.tsx │ └── restartBtn.tsx ├── tsconfig.json └── webpack.config.js └── image └── components.png /.gitignore: -------------------------------------------------------------------------------- 1 | **/node_modules/ 2 | **/dist/ 3 | **/.DS_Store 4 | **/.vscode 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) Microsoft Corporation. All rights reserved. 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 | # TypeScript React Conversion Guide 2 | 3 | This walkthrough illustrates how to adopt TypeScript in an existing React/Babel/Webpack project. We'll start with a TicTacToe project written fully in JavaScript in the `TicTacToe_JS` folder as an example. By the end, you will have a TicTacToe project fully written with TypeScript. 4 | 5 | If you are starting a new React project instead of converting one, you can use [this tutorial](https://github.com/Microsoft/TypeScript-Handbook/blob/master/pages/tutorials/React.md). 6 | 7 | Adopting TypeScript in any project can be broken down into 2 phases: 8 | 9 | * Adding the TypeScript compiler (tsc) to your build pipeline. 10 | * Converting JavaScript files into TypeScript files. 11 | 12 | ## Understand the existing JavaScript project 13 | 14 | Before we dive into TypeScript adoption, let's take a look at the structure of the TicTacToe app -- it contains a few components and looks like the below (with or without TypeScript). 15 | 16 |

17 | 18 |

19 | 20 | As shown in `package.json`, the app already includes React/ReactDOM, Webpack as the bundler & task runner, and the [babel-loader](https://github.com/babel/babel-loader) Webpack plugin to use Babel for ES6 and JSX transpilation. The project initially has the below overall layout before we adopt TypeScript: 21 | 22 | ```txt 23 | TicTacToe_JS / 24 | |---- css/ // css style sheets 25 | |---- src/ // source files 26 | |---- app.jsx // the App React component 27 | |---- board.jsx // the TicTacToe Board React component 28 | |---- constants.js // some shared constants 29 | |---- gameStateBar.jsx // GameStatusBar React component 30 | |---- restartBtn.jsx // RestartBtn React component 31 | |---- .babelrc // a list of babel presets 32 | |---- index.html // web page for our app 33 | |---- package.json // node package configuration file 34 | |---- webpack.config.js // Webpack configuration file 35 | ``` 36 | 37 | ## Add TypeScript compiler to build pipeline 38 | 39 | ### Install dependencies 40 | 41 | To get started, open a terminal and `cd` to the `TicTacToe_JS` folder. Install all dependencies defined in `package.json`. 42 | 43 | ```sh 44 | cd TicTacToe_JS 45 | npm install 46 | ``` 47 | 48 | Additionally, install TypeScript (3 or higher), [ts-loader](https://www.npmjs.com/package/ts-loader) and [source-map-loader](https://www.npmjs.com/package/source-map-loader) as dev dependencies if you haven't. ts-loader is a Webpack plugin that helps you compile TypeScript code to JavaScript, much like babel-loader for Babel. There are also other alternative loaders for TypeScript! Source-map-loader adds source map support for debugging. 49 | 50 | ```sh 51 | npm install --save-dev typescript ts-loader source-map-loader 52 | ``` 53 | 54 | Get the type declaration files (.d.ts files) from [@types](https://blogs.msdn.microsoft.com/typescript/2016/06/15/the-future-of-declaration-files/) for any library in use. For this project, we have React and ReactDOM. 55 | 56 | ```sh 57 | npm install --save @types/react @types/react-dom 58 | ``` 59 | 60 | If you are using an older version of React or ReacDOM that is incompatible with the latest .d.ts files from @types, you can specify a version number for `@types/react` or `@types/react-dom` in `package.json`. 61 | 62 | ### Configure TypeScript 63 | 64 | Next, configure TypeScript by creating a `tsconfig.json` file in the `TicTacToe_JS` folder, and add: 65 | 66 | ```json5 67 | { 68 | "compilerOptions": { 69 | "outDir": "./dist/", // path to output directory 70 | "sourceMap": true, // allow sourcemap support 71 | "strictNullChecks": true, // enable strict null checks as a best practice 72 | "module": "es6", // specify module code generation 73 | "jsx": "react", // use typescript to transpile jsx to js 74 | "target": "es5", // specify ECMAScript target version 75 | "allowJs": true // allow a partial TypeScript and JavaScript codebase 76 | 77 | }, 78 | "include": [ 79 | "./src/" 80 | ] 81 | } 82 | ``` 83 | 84 | You can edit some of the options or add more based on your project's requirements. See more options in the full list of [compiler options](https://www.typescriptlang.org/docs/handbook/compiler-options.html). 85 | 86 | ### Set up build pipeline 87 | 88 | To add TypeScript compilation as part of our build process, you need to modify the Webpack config file `webpack.config.js`. This section is specific to Webpack. However, if you are using a different task runner (e.g. Gulp) for your React/Babel project, the idea is the same - replace the Babel build step with TypeScript, as TypeScript also offers transpiling to lower ECMAScript versions and JSX transpilation with a shorter build time in most cases. If you wish, you can also keep Babel by adding a TypeScript build step before Babel and feeding its output to Babel. 89 | 90 | Generally, we need to change `webpack.config.js` in a few ways: 91 | 92 | 1. Expand the module resolution extensions to include `.ts` and `.tsx` files. 93 | 2. Replace `babel-loader` with `ts-loader`. 94 | 3. Add source-map support. 95 | 96 | Let's modify `webpack.config.js` with the below: 97 | 98 | ```js 99 | module.exports = { 100 | // change to .tsx if necessary 101 | entry: './src/app.jsx', 102 | output: { 103 | filename: './bundle.js' 104 | }, 105 | resolve: { 106 | // changed from extensions: [".js", ".jsx"] 107 | extensions: [".ts", ".tsx", ".js", ".jsx"] 108 | }, 109 | module: { 110 | rules: [ 111 | // changed from { test: /\.jsx?$/, use: { loader: 'babel-loader' }, exclude: /node_modules/ }, 112 | { test: /\.(t|j)sx?$/, use: { loader: 'ts-loader' }, exclude: /node_modules/ }, 113 | 114 | // addition - add source-map support 115 | { enforce: "pre", test: /\.js$/, exclude: /node_modules/, loader: "source-map-loader" } 116 | ] 117 | }, 118 | externals: { 119 | "react": "React", 120 | "react-dom": "ReactDOM", 121 | }, 122 | // addition - add source-map support 123 | devtool: "source-map" 124 | } 125 | ``` 126 | 127 | You can delete `.babelrc` and all Babel dependencies from `package.json` if you no longer need them. 128 | 129 | Note that if you plan to adopt TypeScript in the entry file, you should change `entry: './src/app.jsx',` to `entry: './src/app.tsx',` as well. For the time being, we will keep it as `app.jsx`. 130 | 131 | You now have the build pipeline correctly set up with TypeScript handling the transpilation. Try bundling the app with the following command and then open `index.html` in a browser: 132 | 133 | ```sh 134 | npx webpack 135 | ``` 136 | 137 | > We are assuming you are using `npx` addition for `npm` to [execute npm packages directly](http://blog.npmjs.org/post/162869356040/introducing-npx-an-npm-package-runner) 138 | 139 | ## Transition from JS(X) to TS(X) 140 | 141 | In this part, we will walk through the following steps progressively, 142 | 143 | 1. The minimum steps of converting one module to TypeScript. 144 | 2. Adding types in one module to get richer type checking. 145 | 3. Fully adopting TypeScript in the entire codebase. 146 | 147 | While you get the most out of TypeScript by fully adopting it across your codebase, understanding each of the three steps comes in handy as you decide what to do in case you have certain part of your JavaScript codebase you want to leave as-is (think legacy code that no one understands). 148 | 149 | ### Minimum transition steps 150 | 151 | Let's look at `gameStateBar.jsx` as an example. 152 | 153 | Step one is to rename `gameStateBar.jsx` to `gameStateBar.tsx`. If you are using any editor with TypeScript support such as [Visual Studio Code](https://code.visualstudio.com/), you should be able to see a few complaints from your editor. 154 | 155 | On line 1 `import React from "react";`, change the import statement to `import * as React from "react"`. This is because while importing a CommonJS module, Babel assumes `modules.export` as default export, while TypeScript does not. 156 | 157 | On line 3 `export class GameStateBar extends React.Component {`, change the class declaration to `export class GameStateBar extends React.Component {`. The type declaration of `React.Component` uses [generic types](https://www.typescriptlang.org/docs/handbook/generics.html) and requires providing the types for the property and state object for the component. The use of `any` allows us to pass in any value as the property or state object, which is not useful in terms of type checking but suffices as minimum effort to appease the compiler. 158 | 159 | By now, ts-loader should be able to successfully compile this TypeScript component to JavaScript. Again, try bundling the app with the following command and then open `index.html` in a browser, 160 | 161 | ```sh 162 | npx webpack 163 | ``` 164 | 165 | ### Add types 166 | 167 | The more type information provided to TypeScript, the more powerful its type checking is. As a best practice, we recommend providing types for all declarations. We will again use the `gameStateBar` component as an example. 168 | 169 | For any `React.Component`, we should properly define the types of the property and state object. The `gameStateBar` component has no properties, therefore we can use `{}` as type. 170 | 171 | The state object contains only one property `gameState` which shows the game status (either nothing, someone wins, or draw). Given `gameState` can only have certain known string literal values, let's use [string literal type](https://www.typescriptlang.org/docs/handbook/advanced-types.html) and define the interface as follow before the class declaration. 172 | 173 | ```ts 174 | interface GameStateBarState { 175 | gameState: "" | "X Wins!" | "O Wins!" | "Draw"; 176 | } 177 | ``` 178 | 179 | With the defined interface, change the `GameStateBar` class declaration, 180 | 181 | ```ts 182 | export class GameStateBar extends React.Component<{}, GameStateBarState> {...} 183 | ``` 184 | 185 | Now, supply type information for its members. Note that providing types to all declarations is not required, but recommended for better type coverage. 186 | 187 | ```ts 188 | // add types for params 189 | constructor(props: {}) {...} 190 | handleGameStateChange(e: CustomEvent) {...} 191 | handleRestart(e: Event) {...} 192 | 193 | // add types in arrow functions 194 | componentDidMount() { 195 | window.addEventListener("gameStateChange", (e: CustomEvent) => this.handleGameStateChange(e)); 196 | window.addEventListener("restart", (e: CustomEvent) => this.handleRestart(e)); 197 | } 198 | 199 | // add types in arrow functions 200 | componentWillUnmount() { 201 | window.removeEventListener("gameStateChange", (e: CustomEvent) => this.handleGameStateChange(e)); 202 | window.removeEventListener("restart", (e: CustomEvent) => this.handleRestart(e)); 203 | } 204 | ``` 205 | 206 | To use stricter type checking, you can also specify useful [compiler options](https://www.typescriptlang.org/docs/handbook/compiler-options.html) in `tsconfig.json`. For example, `noImplicitAny` is a recommended option that triggers the compiler to error on expressions and declarations with an implied `any` type. 207 | 208 | You can also add [private/protected modifier](https://www.typescriptlang.org/docs/handbook/classes.html) to class members for access control. Let's mark `handleGameStateChange` and `handleRestart` as `private` as they are internal to `gameStateBar`. 209 | 210 | ```ts 211 | private handleGameStateChange(e: CustomEvent) {...} 212 | private handleRestart(e: Event) {...} 213 | ``` 214 | 215 | Again, try bundling the app with the following command and then open `index.html` in a browser, 216 | 217 | ```sh 218 | npx webpack 219 | ``` 220 | 221 | ### Adopt TypeScript in the entire codebase 222 | 223 | Adopting TypeScript in the entire codebase is more or less repeating the previous two steps for all js(x) files. You may need to make changes additional to what is mentioned above while converting perfectly valid JavaScript to TypeScript. However the TypeScript compiler and your editor (if it has TypeScript support) should give you useful tips and error messages. For instance, parameters can be optional in JavaScript, but in TypeScript all [optional parameter](https://www.typescriptlang.org/docs/handbook/functions.html) must be marked with `?` 224 | 225 | You can see the fully converted TicTacToe project in the `TicTacToe_TS` folder. Build the app with, 226 | 227 | ```sh 228 | npm install 229 | npx webpack 230 | ``` 231 | 232 | Run the app by opening `index.html`. 233 | -------------------------------------------------------------------------------- /TicTacToe_JS/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "@babel/preset-env", 4 | "@babel/preset-react" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /TicTacToe_JS/css/style.css: -------------------------------------------------------------------------------- 1 | html { 2 | box-sizing: border-box; 3 | } 4 | * { 5 | box-sizing: inherit; 6 | } 7 | 8 | .app { 9 | font-family: 'Open Sans', sans-serif; 10 | margin: 100px; 11 | width: 500px; 12 | margin-left: auto; 13 | margin-right: auto; 14 | -webkit-touch-callout: none; 15 | -webkit-user-select: none; 16 | -khtml-user-select: none; 17 | -moz-user-select: none; 18 | -ms-user-select: none; 19 | user-select: none; 20 | } 21 | 22 | .board { 23 | position: relative; 24 | width: 500px; 25 | height:500px; 26 | } 27 | 28 | .cell { 29 | float: left; 30 | width: 33.3333%; 31 | height: 33.3333%; 32 | line-height: 166.67px; 33 | color: black; 34 | font-size: 90pt; 35 | text-align: center; 36 | border-color: orangered; 37 | border-width: 3px; 38 | } 39 | .cell.top { 40 | border-bottom-style:solid; 41 | } 42 | .cell.bottom { 43 | border-top-style:solid; 44 | } 45 | .cell.left { 46 | border-right-style:solid; 47 | } 48 | .cell.right { 49 | border-left-style:solid; 50 | } 51 | 52 | .X{ 53 | animation-name: appear; 54 | animation-duration: .3s; 55 | } 56 | .O{ 57 | animation-name: appear; 58 | animation-duration: .3s; 59 | animation-delay:.3s; 60 | animation-fill-mode: forwards; 61 | opacity: 0; 62 | } 63 | @keyframes appear { 64 | from { font-size: 90pt; opacity: 0;} 65 | to { font-size: 100pt; opacity: 1;} 66 | } 67 | 68 | .description{ 69 | cursor:pointer; 70 | font-size:25px; 71 | font-weight:bold; 72 | padding:15px 0px; 73 | position: relative; 74 | display: inline-block; 75 | width: 200px; 76 | text-align: center; 77 | margin-top: 30px; 78 | margin-right: -35px; 79 | } 80 | .t1{ 81 | margin-left: 60px; 82 | } 83 | .t2{ 84 | margin-right: 60px; 85 | } 86 | 87 | .gameStateBar { 88 | text-align: center; 89 | font-size: 60px; 90 | font-weight: bold; 91 | height: 60px; 92 | } 93 | 94 | .restartBtn { 95 | box-shadow: 3px 3px 9px 2px #54a3f7; 96 | background-color:#007dc1; 97 | border-radius:28px; 98 | border:1px solid #124d77; 99 | cursor:pointer; 100 | color:#ffffff; 101 | font-size:25px; 102 | font-weight:bold; 103 | padding:15px 36px; 104 | text-decoration:none; 105 | text-shadow:0px 0px 7px #154682; 106 | position: relative; 107 | display: block; 108 | margin: 40px auto; 109 | width: 160px; 110 | text-align: center; 111 | } 112 | .restartBtn:hover { 113 | background-color:#0061a7; 114 | } 115 | .restartBtn:active { 116 | position:relative; 117 | top:1px; 118 | } 119 | -------------------------------------------------------------------------------- /TicTacToe_JS/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | TicTacToe with TypeScript and React 6 | 7 | 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /TicTacToe_JS/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-tic-tac-toe", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "dependencies": { 6 | "react": "^16.12.0", 7 | "react-dom": "^16.12.0" 8 | }, 9 | "devDependencies": { 10 | "@babel/core": "^7.0.0", 11 | "@babel/preset-env": "^7.0.0", 12 | "@babel/preset-react": "^7.0.0", 13 | "babel-loader": "^8.0.0", 14 | "webpack": "^4.41.5", 15 | "webpack-cli": "^3.3.10" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /TicTacToe_JS/src/app.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { render } from "react-dom"; 3 | import { Board } from "./board"; 4 | import { RestartBtn } from "./restartBtn"; 5 | import { GameStateBar } from "./gameStateBar"; 6 | 7 | class App extends React.Component { 8 | render() { 9 | return ( 10 |
11 | 12 |
13 | Player(X) 14 | Computer(O) 15 |
16 | 17 | 18 |
19 | ) 20 | } 21 | } 22 | 23 | render( 24 | , document.getElementById("content") 25 | ); 26 | -------------------------------------------------------------------------------- /TicTacToe_JS/src/board.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { playerCell, aiCell } from "./constants"; 3 | 4 | export class Board extends React.Component { 5 | 6 | constructor(props) { 7 | super(props); 8 | this.state = this.getInitState(); 9 | } 10 | 11 | getInitState() { 12 | let cells = Array.apply(null, Array(9)).map(() => ""); 13 | return {cells: cells, gameState: ""} 14 | } 15 | 16 | resetState() { 17 | this.setState(this.getInitState()); 18 | } 19 | 20 | componentDidMount() { 21 | window.addEventListener("restart", () => this.resetState()); 22 | } 23 | 24 | componentWillUnmount() { 25 | window.removeEventListener("restart", () => this.resetState()); 26 | } 27 | 28 | // Fire a global event notifying GameState changes 29 | handleGameStateChange(newState) { 30 | var event = new CustomEvent("gameStateChange", { "detail": this.state.gameState }); 31 | event.initEvent("gameStateChange", false, true); 32 | window.dispatchEvent(event); 33 | } 34 | 35 | // check the game state - use the latest move 36 | checkGameState(cells, latestPos, latestVal) { 37 | if (this.state.gameState !== "") { 38 | return this.state.gameState; 39 | } 40 | 41 | // check row 42 | let result = this.check3Cells(cells, 3 * Math.floor(latestPos / 3), 43 | 3 * Math.floor(latestPos / 3) + 1, 3 * Math.floor(latestPos/3) + 2); 44 | if (result) { 45 | return result; 46 | } 47 | 48 | // check col 49 | result = this.check3Cells(cells, latestPos % 3, latestPos % 3 + 3, latestPos % 3 + 6); 50 | if (result) { 51 | return result; 52 | } 53 | 54 | // check diag 55 | result = this.check3Cells(cells, 0, 4, 8); 56 | if (result) { 57 | return result; 58 | } 59 | result = this.check3Cells(cells, 2, 4, 6); 60 | if (result) { 61 | return result; 62 | } 63 | 64 | // check draw - if all cells are filled 65 | if (this.findAllEmptyCells(cells).length === 0) { 66 | return "Draw"; 67 | } 68 | 69 | return ""; 70 | } 71 | 72 | // check if 3 cells have same non-empty val - return the winner state; otherwise undefined 73 | check3Cells(cells, pos0, pos1, pos2) { 74 | if (cells[pos0] === cells[pos1] && 75 | cells[pos1] === cells[pos2] && 76 | cells[pos0] !== "") { 77 | if (cells[pos0] === "X") { 78 | return "X Wins!"; 79 | } 80 | return "O Wins!"; 81 | } 82 | else { 83 | return undefined; 84 | } 85 | } 86 | 87 | // list all empty cell positions 88 | findAllEmptyCells(cells) { 89 | return cells.map((v, i) => { 90 | if (v === "") { 91 | return i; 92 | } 93 | else { 94 | return -1; 95 | } 96 | }).filter(v => { return v !== -1 }); 97 | } 98 | 99 | // make a move 100 | move(pos, val, callback) { 101 | if (this.state.gameState === "" && 102 | this.state.cells[pos] === "") { 103 | let newCells = this.state.cells.slice(); 104 | newCells[pos] = val; 105 | let oldState = this.state.gameState; 106 | this.setState({cells: newCells, gameState: this.checkGameState(newCells, pos, val)}, () => { 107 | if (this.state.gameState !== oldState) { 108 | this.handleGameStateChange(this.state.gameState); 109 | } 110 | if (callback) { 111 | callback.call(this); 112 | } 113 | }); 114 | } 115 | } 116 | 117 | // handle a new move from player 118 | handleNewPlayerMove(pos) { 119 | this.move(pos, playerCell, () => { 120 | // AI make a random move following player's move 121 | let emptyCells = this.findAllEmptyCells(this.state.cells); 122 | let pos = emptyCells[Math.floor(Math.random() * emptyCells.length)]; 123 | this.move(pos, aiCell); 124 | }); 125 | } 126 | 127 | render() { 128 | var cells = this.state.cells.map((v, i) => { 129 | return ( 130 | this.handleNewPlayerMove(i)} /> 131 | ) 132 | } ); 133 | 134 | return ( 135 |
136 | {cells} 137 |
138 | ) 139 | } 140 | } 141 | 142 | class Cell extends React.Component { 143 | 144 | // position of cell to className 145 | posToClassName(pos) { 146 | let className = "cell"; 147 | switch (Math.floor(pos / 3)) { 148 | case 0: 149 | className += " top"; 150 | break; 151 | case 2: 152 | className += " bottom"; 153 | break; 154 | default: break; 155 | } 156 | switch (pos % 3) { 157 | case 0: 158 | className += " left"; 159 | break; 160 | case 2: 161 | className += " right"; 162 | break; 163 | default: 164 | break; 165 | } 166 | return className; 167 | } 168 | 169 | handleClick(e) { 170 | this.props.handleMove(); 171 | } 172 | 173 | render() { 174 | let name = this.props.val; 175 | if (this.props.val === "") { 176 | name = ""; 177 | } 178 | return
this.handleClick(e)}> 179 |
{this.props.val}
180 |
181 | } 182 | } 183 | -------------------------------------------------------------------------------- /TicTacToe_JS/src/constants.js: -------------------------------------------------------------------------------- 1 | export const playerCell = "X"; 2 | export const aiCell = "O"; 3 | -------------------------------------------------------------------------------- /TicTacToe_JS/src/gameStateBar.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | export class GameStateBar extends React.Component { 4 | 5 | constructor(props) { 6 | super(props); 7 | this.state = {gameState: ""}; 8 | } 9 | 10 | handleGameStateChange(e) { 11 | this.setState({gameState: e.detail}); 12 | } 13 | 14 | handleRestart(e) { 15 | this.setState({gameState: ""}); 16 | } 17 | 18 | componentDidMount() { 19 | window.addEventListener("gameStateChange", e => this.handleGameStateChange(e)); 20 | window.addEventListener("restart", e => this.handleRestart(e)); 21 | } 22 | 23 | componentWillUnmount() { 24 | window.removeEventListener("gameStateChange", e => this.handleGameStateChange(e)); 25 | window.removeEventListener("restart", e => this.handleRestart(e)); 26 | } 27 | 28 | render() { 29 | return ( 30 |
{this.state.gameState}
31 | ) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /TicTacToe_JS/src/restartBtn.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | export class RestartBtn extends React.Component { 4 | 5 | // Fire a global event notifying restart of game 6 | handleClick(e) { 7 | var event = document.createEvent("Event"); 8 | event.initEvent("restart", false, true); 9 | window.dispatchEvent(event); 10 | } 11 | 12 | render() { 13 | return this.handleClick(e)}> 14 | Restart 15 | ; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /TicTacToe_JS/webpack.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | entry: './src/app.jsx', 3 | output: { 4 | filename: './bundle.js' 5 | }, 6 | resolve: { 7 | extensions: [".js", ".jsx"] 8 | }, 9 | module: { 10 | rules: [ 11 | { test: /\.jsx?$/, exclude: /node_modules/, use: { loader: 'babel-loader' } } 12 | ] 13 | }, 14 | externals: { 15 | "react": "React", 16 | "react-dom": "ReactDOM", 17 | }, 18 | } 19 | -------------------------------------------------------------------------------- /TicTacToe_TS/css/style.css: -------------------------------------------------------------------------------- 1 | html { 2 | box-sizing: border-box; 3 | } 4 | * { 5 | box-sizing: inherit; 6 | } 7 | 8 | .app { 9 | font-family: 'Open Sans', sans-serif; 10 | margin: 100px; 11 | width: 500px; 12 | margin-left: auto; 13 | margin-right: auto; 14 | -webkit-touch-callout: none; 15 | -webkit-user-select: none; 16 | -khtml-user-select: none; 17 | -moz-user-select: none; 18 | -ms-user-select: none; 19 | user-select: none; 20 | } 21 | 22 | .board { 23 | position: relative; 24 | width: 500px; 25 | height:500px; 26 | } 27 | 28 | .cell { 29 | float: left; 30 | width: 33.3333%; 31 | height: 33.3333%; 32 | line-height: 166.67px; 33 | color: black; 34 | font-size: 90pt; 35 | text-align: center; 36 | border-color: orangered; 37 | border-width: 3px; 38 | } 39 | .cell.top { 40 | border-bottom-style:solid; 41 | } 42 | .cell.bottom { 43 | border-top-style:solid; 44 | } 45 | .cell.left { 46 | border-right-style:solid; 47 | } 48 | .cell.right { 49 | border-left-style:solid; 50 | } 51 | 52 | .X{ 53 | animation-name: appear; 54 | animation-duration: .3s; 55 | } 56 | .O{ 57 | animation-name: appear; 58 | animation-duration: .3s; 59 | animation-delay:.3s; 60 | animation-fill-mode: forwards; 61 | opacity: 0; 62 | } 63 | @keyframes appear { 64 | from { font-size: 90pt; opacity: 0;} 65 | to { font-size: 100pt; opacity: 1;} 66 | } 67 | 68 | .description{ 69 | cursor:pointer; 70 | font-size:25px; 71 | font-weight:bold; 72 | padding:15px 0px; 73 | position: relative; 74 | display: inline-block; 75 | width: 200px; 76 | text-align: center; 77 | margin-top: 30px; 78 | margin-right: -35px; 79 | } 80 | .t1{ 81 | margin-left: 60px; 82 | } 83 | .t2{ 84 | margin-right: 60px; 85 | } 86 | 87 | .gameStateBar { 88 | text-align: center; 89 | font-size: 60px; 90 | font-weight: bold; 91 | height: 60px; 92 | } 93 | 94 | .restartBtn { 95 | box-shadow: 3px 3px 9px 2px #54a3f7; 96 | background-color:#007dc1; 97 | border-radius:28px; 98 | border:1px solid #124d77; 99 | cursor:pointer; 100 | color:#ffffff; 101 | font-size:25px; 102 | font-weight:bold; 103 | padding:15px 36px; 104 | text-decoration:none; 105 | text-shadow:0px 0px 7px #154682; 106 | position: relative; 107 | display: block; 108 | margin: 40px auto; 109 | width: 160px; 110 | text-align: center; 111 | } 112 | .restartBtn:hover { 113 | background-color:#0061a7; 114 | } 115 | .restartBtn:active { 116 | position:relative; 117 | top:1px; 118 | } 119 | -------------------------------------------------------------------------------- /TicTacToe_TS/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | TicTacToe with TypeScript and React 6 | 7 | 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /TicTacToe_TS/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-tic-tac-toe", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "dependencies": { 6 | "@types/react": "^16.9.17", 7 | "@types/react-dom": "^16.9.4", 8 | "react": "^15.4.2", 9 | "react-dom": "^15.4.2" 10 | }, 11 | "devDependencies": { 12 | "source-map-loader": "^0.2.4", 13 | "ts-loader": "^6.2.1", 14 | "typescript": "^3.7.4", 15 | "webpack": "^4.41.5", 16 | "webpack-cli": "^3.3.10" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /TicTacToe_TS/src/app.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { render } from "react-dom"; 3 | import { Board } from "./board"; 4 | import { RestartBtn } from "./restartBtn"; 5 | import { GameStateBar } from "./gameStateBar"; 6 | import { GameState } from "./constants"; 7 | 8 | class App extends React.Component<{}, {}> { 9 | render() { 10 | return ( 11 |
12 | 13 |
14 | Player(X) 15 | Computer(O) 16 |
17 | 18 | 19 |
20 | ) 21 | } 22 | } 23 | 24 | render( 25 | , document.getElementById("content") 26 | ); 27 | -------------------------------------------------------------------------------- /TicTacToe_TS/src/board.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { CellValue, GameState, playerCell, aiCell } from "./constants"; 3 | 4 | interface BoardState { 5 | cells: CellValue[]; 6 | gameState: GameState; 7 | } 8 | 9 | export class Board extends React.Component<{}, BoardState> { 10 | 11 | constructor(props: {}) { 12 | super(props); 13 | this.state = this.getInitState(); 14 | } 15 | 16 | private getInitState(): BoardState { 17 | let cells = Array.apply(null, Array(9)).map(() => ""); 18 | return {cells: cells, gameState: ""} 19 | } 20 | 21 | private resetState(): void { 22 | this.setState(this.getInitState()); 23 | } 24 | 25 | componentDidMount() { 26 | window.addEventListener("restart", () => this.resetState()); 27 | } 28 | 29 | componentWillUnmount() { 30 | window.removeEventListener("restart", () => this.resetState()); 31 | } 32 | 33 | // Fire a global event notifying GameState changes 34 | private handleGameStateChange(newState: GameState) { 35 | var event = new CustomEvent("gameStateChange", { "detail": this.state.gameState }); 36 | event.initEvent("gameStateChange", false, true); 37 | window.dispatchEvent(event); 38 | } 39 | 40 | // check the game state - use the latest move 41 | private checkGameState(cells: CellValue[], latestPos: number, latestVal: CellValue): GameState { 42 | if (this.state.gameState !== "") { 43 | return this.state.gameState; 44 | } 45 | 46 | // check row 47 | let result = this.check3Cells(cells, 3 * Math.floor(latestPos / 3), 48 | 3 * Math.floor(latestPos / 3) + 1, 3 * Math.floor(latestPos/3) + 2); 49 | if (result) { 50 | return result; 51 | } 52 | 53 | // check col 54 | result = this.check3Cells(cells, latestPos % 3, latestPos % 3 + 3, latestPos % 3 + 6); 55 | if (result) { 56 | return result; 57 | } 58 | 59 | // check diag 60 | result = this.check3Cells(cells, 0, 4, 8); 61 | if (result) { 62 | return result; 63 | } 64 | result = this.check3Cells(cells, 2, 4, 6); 65 | if (result) { 66 | return result; 67 | } 68 | 69 | // check draw - if all cells are filled 70 | if (this.findAllEmptyCells(cells).length === 0) { 71 | return "Draw"; 72 | } 73 | 74 | return ""; 75 | } 76 | 77 | // check if 3 cells have same non-empty val - return the winner state; otherwise undefined 78 | private check3Cells(cells: CellValue[], pos0: number, pos1: number, pos2: number): GameState | undefined { 79 | if (cells[pos0] === cells[pos1] && 80 | cells[pos1] === cells[pos2] && 81 | cells[pos0] !== "") { 82 | if (cells[pos0] === "X") { 83 | return "X Wins!"; 84 | } 85 | return "O Wins!"; 86 | } 87 | else { 88 | return undefined; 89 | } 90 | } 91 | 92 | // list all empty cell positions 93 | private findAllEmptyCells(cells : CellValue[]): number[] { 94 | return cells.map((v, i) => { 95 | if (v === "") { 96 | return i; 97 | } 98 | else { 99 | return -1; 100 | } 101 | }).filter(v => { return v !== -1 }); 102 | } 103 | 104 | // make a move 105 | private move(pos: number, val: CellValue, callback?: () => void): void { 106 | if (this.state.gameState === "" && 107 | this.state.cells[pos] === "") { 108 | let newCells = this.state.cells.slice(); 109 | newCells[pos] = val; 110 | let oldState = this.state.gameState; 111 | this.setState({cells: newCells, gameState: this.checkGameState(newCells, pos, val)}, () => { 112 | if (this.state.gameState !== oldState) { 113 | this.handleGameStateChange(this.state.gameState); 114 | } 115 | if (callback) { 116 | callback.call(this); 117 | } 118 | }); 119 | } 120 | } 121 | 122 | // handle a new move from player 123 | private handleNewPlayerMove(pos: number): void { 124 | this.move(pos, playerCell, () => { 125 | // AI make a random move following player's move 126 | let emptyCells = this.findAllEmptyCells(this.state.cells); 127 | let pos = emptyCells[Math.floor(Math.random() * emptyCells.length)]; 128 | this.move(pos, aiCell); 129 | }); 130 | } 131 | 132 | render() { 133 | var cells = this.state.cells.map((v, i) => { 134 | return ( 135 | this.handleNewPlayerMove(i)} /> 136 | ) 137 | } ); 138 | 139 | return ( 140 |
141 | {cells} 142 |
143 | ) 144 | } 145 | } 146 | 147 | interface CellProps extends React.Props { 148 | pos: number; 149 | val: CellValue; 150 | handleMove: () => void; 151 | } 152 | 153 | class Cell extends React.Component { 154 | 155 | // position of cell to className 156 | private posToClassName(pos: number): string { 157 | let className = "cell"; 158 | switch (Math.floor(pos / 3)) { 159 | case 0: 160 | className += " top"; 161 | break; 162 | case 2: 163 | className += " bottom"; 164 | break; 165 | default: break; 166 | } 167 | switch (pos % 3) { 168 | case 0: 169 | className += " left"; 170 | break; 171 | case 2: 172 | className += " right"; 173 | break; 174 | default: 175 | break; 176 | } 177 | return className; 178 | } 179 | 180 | private handleClick(e: React.MouseEvent) { 181 | this.props.handleMove(); 182 | } 183 | 184 | render() { 185 | let name = this.props.val; 186 | if (this.props.val === "") { 187 | name = ""; 188 | } 189 | return
this.handleClick(e)}> 190 |
{this.props.val}
191 |
192 | } 193 | } 194 | -------------------------------------------------------------------------------- /TicTacToe_TS/src/constants.ts: -------------------------------------------------------------------------------- 1 | export type GameState = "" | "X Wins!" | "O Wins!" | "Draw"; 2 | export type CellValue = "" | "X" | "O"; 3 | export const playerCell: CellValue = "X"; 4 | export const aiCell: CellValue = "O"; 5 | -------------------------------------------------------------------------------- /TicTacToe_TS/src/gameStateBar.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { GameState } from "./constants"; 3 | 4 | interface GameStateBarState { 5 | gameState: GameState; 6 | } 7 | 8 | export class GameStateBar extends React.Component<{}, GameStateBarState> { 9 | 10 | constructor(props: {}) { 11 | super(props); 12 | this.state = {gameState: ""}; 13 | } 14 | 15 | private handleGameStateChange(e: CustomEvent) { 16 | this.setState({gameState: e.detail}); 17 | } 18 | 19 | private handleRestart(e: Event) { 20 | this.setState({gameState: ""}); 21 | } 22 | 23 | componentDidMount() { 24 | window.addEventListener("gameStateChange", (e: CustomEvent) => this.handleGameStateChange(e)); 25 | window.addEventListener("restart", (e: CustomEvent) => this.handleRestart(e)); 26 | } 27 | 28 | componentWillUnmount() { 29 | window.removeEventListener("gameStateChange", (e: CustomEvent) => this.handleGameStateChange(e)); 30 | window.removeEventListener("restart", (e: CustomEvent) => this.handleRestart(e)); 31 | } 32 | 33 | render() { 34 | return ( 35 |
{this.state.gameState}
36 | ) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /TicTacToe_TS/src/restartBtn.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | export class RestartBtn extends React.Component<{}, {}> { 4 | 5 | // Fire a global event notifying restart of game 6 | private handleClick(e: React.MouseEvent) { 7 | var event = document.createEvent("Event"); 8 | event.initEvent("restart", false, true); 9 | window.dispatchEvent(event); 10 | } 11 | 12 | render() { 13 | return this.handleClick(e)}> 14 | Restart 15 | ; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /TicTacToe_TS/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "./dist/", // path to output directory 4 | "sourceMap": true, // allow sourcemap support 5 | "strictNullChecks": true, // enable strict null checks as a best practice 6 | "module": "es6", // specifiy module code generation 7 | "jsx": "react", // use typescript to transpile jsx to js 8 | "target": "es5", // specify ECMAScript target version 9 | "allowJs": true, // allow a partial TypeScript and JavaScript codebase 10 | "noImplicitAny": true // disallow implicit any type 11 | }, 12 | "include": [ 13 | "./src/" 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /TicTacToe_TS/webpack.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | // change to .tsx if necessary 3 | entry: './src/app.tsx', 4 | output: { 5 | filename: './bundle.js' 6 | }, 7 | resolve: { 8 | // changed from extensions: [".js", ".jsx"] 9 | extensions: [".ts", ".tsx", ".js", ".jsx"] 10 | }, 11 | module: { 12 | rules: [ 13 | // changed from { test: /\.jsx?$/, use: { loader: 'babel-loader' }, exclude: /node_modules/ }, 14 | { test: /\.(t|j)sx?$/, use: { loader: 'ts-loader' }, exclude: /node_modules/ }, 15 | 16 | // newline - add source-map support 17 | { enforce: "pre", test: /\.js$/, exclude: /node_modules/, loader: "source-map-loader" } 18 | ] 19 | }, 20 | externals: { 21 | "react": "React", 22 | "react-dom": "ReactDOM", 23 | }, 24 | // newline - add source-map support 25 | devtool: "source-map" 26 | } 27 | -------------------------------------------------------------------------------- /image/components.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/TypeScript-React-Conversion-Guide/4ce20fc58c8285affd46f2876b1a8f712ba46288/image/components.png --------------------------------------------------------------------------------