When Minesweeper shipped with Windows 3.1 it wasn’t simply to entertain. The addictive little game masked an ulterior motive: introducing users to the right button of the mouse. The two-button mouse is old news by now, but Minesweeper remains just the right size for a different introduction.
Over the next fifteen minutes, we’ll build a Minesweeper clone (github) that uses TypeScript, React, and Redux to build a robust browser application. It’s a simple, synchronous project, but it’s enough to introduce patterns that use Redux and TypeScript to help tame the complexity of much larger applications.
No time for configuration—the clock is ticking! We can use create-react-app-typescript to set up a project with sensible defaults. Fine tuning can come later.
$ create-react-app minesweeper — scripts-version=react-scripts-ts$ cd minesweeper$ yarn install
We’ll also need to add redux
, redux-react
, and type definitions for the latter (redux
brings its own).
$ yarn add redux react-redux$ yarn add @types/react-redux
Next, we’re going to cheat (but only a little). While we’ll use React for the frontend UI and Redux for persistence, we can save time by borrowing an existing model of the game itself.
This particular implementation happens to be friendly, local, and synchronous, but it’s easy to imagine moving MinesweeperAPI
to the far end of an HTTP request. Its behavior would remain the same in either case: given the current state of the Game
and a request to reveal or flag a single cell, the MinesweeperAPI
will return a new state updated to reflect the consequences of the request.
Next, we’ll need to tell TypeScript about the “shape” of the Game
. This consists of Options
—the size of the board and how many mines are hidden on it—a moveCount
, and several indices comprising the state of the board. The finished type declaration documents this nicely:
We’ve got a game—we just need somewhere to put it. We’ll prepare Redux to store and update the state of the Game
by mirroring the public MinesweeperAPI
in typed actions of our own:
Except for the type definitions these look just like the action creators we would write in vanilla JavaScript. But by stating our assumptions up front, the TypeScript compiler can enforce the shape of these actions elsewhere in the app. If we try to create an action that isn’t declared in the Action
union, the program won’t compile.
We’ll get even more benefit from static typing inside the store. One of the challenges of scaling Redux is the complexity of increasingly “deep” reducers, accessors, and state. By declaring their shape preemptively, the TypeScript compiler can warn us about typos, missing parameters, and broken references.
To keep things simple, we’ll build the store around the existing Game
type from the domain model. Even with this relatively flat data-structure, we’ll sleep easier at night with TypeScript’s assurance that the rest of the app is following the rules.
Break it! Try accessing action.options
within the 'REVEAL_LOCATION'
branch, and check out the resulting compiler error.
Speaking of “check out”, the clock! Time’s tight, but we still have time for a bit of civic beautification.
To get things going, let’s connect
the Redux store to the component tree by wrapping a [Provider](https://github.com/reactjs/react-redux/blob/4c2670dc11cc067ef106f6c527e6e8b9d47f8af8/docs/api.md#provider-store)
around our root element. This works exactly as in JavaScript:
ReactDOM.render(<Provider store={store}><App /></Provider>,document.getElementById('root') as HTMLElement);
Next, we’ll scaffold a root application component and [connect](https://github.com/reactjs/react-redux/blob/4c2670dc11cc067ef106f6c527e6e8b9d47f8af8/docs/api.md#connect)
it to the provided store
.
TypeScript adds several new facets here. The mapStateToProps
and mapDispatchToProps
functions passed into connect
each have their own type declarations, whose intersection represents the component’s own Props
. When the dust settles,<App />
will receive the entire Redux Game
plus the three available actions and a grid
array to simplify rendering. In return for the added ceremony, we get type-safety all the way down: unknown actions, malformed props, and undefined references will now all trigger errors at compile time.
<App />
is wired into the store but isn’t much to look at yet. To fix that, let’s create a component representing a single cell on the grid:
Leaving CSS and rendering optimization as for a future exercise (we’re on a schedule, here!) this looks nearly like it would in JavaScript. Inference is the name of the game: once TypeScript understands the boundaries of the application, it can infer the correct types for most of the details inside. This won’t much matter to our little speed-run, but it will save much ceremony as the project scales up.
The one thing the compiler won’t infer nicely are new data crossing the application boundary. When a cell is clicked, for instance, we need to tell TypeScript what sorts of events an independent event handler is prepared to receive:
e: React.MouseEvent<HTMLButtonElement>
The compiler can now verify that we’re handling the event correctly. If we pass on a KeyboardEvent
or attach the handler to something other than a <button />
, we’ll be greeted with an error at compile time.
We’re nearly there! We need two more things to get the game running inside the <App />
: a [componentDidMount](https://reactjs.org/docs/react-component.html#componentdidmount)
hook to create the initial game state, and a render
method to put the CellComponent
grid on the screen.
This is just JavaScript—with all the types declared upstream, TypeScript will infer the details the rest of the way down.
Pencils down. 14:59. Tack on a beautiful design, and there it is: statically-verified Minesweeper in just under 15 minutes. We’ve left plenty of details for other TypeScript tutorials, but the patterns introduced here can carry over to projects of all sizes and shapes.
The finished game isn’t the prettiest thing. No tests, suboptimal rendering, and plenty of opportunity for the design team, but we’re finally to the part where TypeScript shines brightest. As the project evolves, all the assumptions embodied in our type declarations will be preserved for reference—and backed by a friendly warnings anytime something breaks the rules.
Check out the finished project on Github!