Building 3D 2048 game with Vue, Three.js and TypeScript (part 1)

Building 3D 2048 game with Vue, Three.js and TypeScript (part 1)

How to create a 2048 3D game with Vue, Three.js and TypeScript - We're going to utilize all these listed tools, to create our own implementation of classic 2048 board game... in 3D

How to create a 2048 3D game with Vue, Three.js and TypeScript - We're going to utilize all these listed tools, to create our own implementation of classic 2048 board game... in 3D

So, this post is going to be a little different. Namely, instead of being just standard-grade content, it's going to be a nice tutorial and kind-of documentation for my personal project, which is in making at the time of writing this post. I hope I managed to somehow get you interested by now, whether it's by this little description or by the title itself. So, what are we going to build?

If you stumbled upon this article, I guess you already have some knowledge about Vue (JS framework), TypeScript (statically-typed compile-to-JS language) and maybe even Three.js(WebGL library). If you don't, it's recommended that you take a look at their official docs, or search for some other resources, before continuing. I will, however, try to explain all the details as well as I possibly can.

In this post (and a few future ones), we're going to utilize all these listed tools, to create our own implementation of classic 2048 board game... in 3D! We'll use Three.js for 3D stuff, Vue for UI and as an abstraction layer from our 3D scene, and TypeScript for great development experience. With that said, in this exact article, we're going to only set up our project - its basic structure, UI and a simple 3D scene, to have some basic stuff ready for the future development! So, bear with me, as we'll be creating our own 3D game!

Example 2048 game board - taken from


We'll first set up our project structure, with all the tools and configs needed. For that, I'll use Vue CLI to bootstrap the project more quickly.

yarn global add @vue/cli

We'll install the Vue CLI globally - the recommended way. In this way, you can later utilize the same CLI for all your upcoming Vue projects. Also, throughout this tutorial, I'm using Yarn instead of NPM as my package-manager-of-choice. If you decide to use Yarn too, just remember to have your PATH variable configured correctly.

To actually set up the project, you'll need to invoke Vue CLI create command with the name of your project (and it's later folder name) as an argument.

vue create 2048

The CLI will guide you through the setup process. Be sure to pick the features manually. In my case, I'm using Babel, TypeScript (together with class components), Vuex, PWA and ESLint, as they'll definitely be useful later. Then, open your favorite code editor and get into the project's folder.

cd ./2048

1As a side-note, you might want to know that Vue CLI also provides a nice UI for creating and managing your Vue projects. With it, you can add dependencies, plugins, configure and run your tasks and more! Thus, it could be quite helpful in our development process. You can open it with a simple command:

vue ui


So, as we've already set up our project, it's time to make a little overview of what's inside. As far as I know, the configuration files shouldn't require any changes, thus, we'll be taking a look at the project files only. But, before that - package.json!

Personally, I run Vue CLI, so that all config files are separate, outside of package.json. In this way, it's much less bloated, leaving us only with some basic data, dependencies list, and a bunch of scripts.

// ...
    "scripts": {
        "serve": "vue-cli-service serve",
        "build": "vue-cli-service build",
        "lint": "vue-cli-service lint"
// ...

"serve" and "build" tasks are provided by default, whatever the setup you've entered. The first one uses HMR, effectively improving our development workflow, while the second one creates optimized output builds. If you turned on the linter during the setup process, you'll end up with one additional "lint" task, specifically for running the linter itself. I think it's worth noting that even if I'm using TypeScript for this project, I chose ESLint for my linter instead of TSLint. ESLint support for TypeScript is constantly getting better, and with a superior number of features, it's an option to consider. Of course, Vue CLI will be happy with any one of those.

Leaving all config files behind, we go straight to the public folder. Here, the two most important files are index.html and manifest.json. index.html, beyond its basic functionalities, serves as a template file for our Vue builds. Bundled code will be put in there automatically. Also, it's a great place for external fonts and CSS to be loaded if necessary.

<!-- ... -->
            <!--JavaScript unavailable message-->
    <div id="app"></div>
    <!-- built files will be auto injected -->
<!-- ... -->

manifest.json will only appear in case you configure the PWA option. It's mainly just a standard JSON file, containing some metadata about the given app to make PWA functionality work correctly. Here, things that we might want to configure later include our color settings and icons. Still, it's a pretty easy and straight-forward process.

  "name": "2048",
  "short_name": "2048",
  "icons": [
      "src": "./img/icons/android-chrome-192x192.png",
      "sizes": "192x192",
      "type": "image/png"
      "src": "./img/icons/android-chrome-512x512.png",
      "sizes": "512x512",
      "type": "image/png"
  "start_url": "./index.html",
  "display": "standalone",
  "background_color": "#000000",
  "theme_color": "#4DBA87"

Now, let's go to the most important directory of the whole project - src. Here, all the files we'll be interacting the most with are located. It's important to notice that the use of TypeScript with Vue is still catching up, both in popularity and support, and thus, you might find some things that are new to you. So, please, bear with me for this one.

We start from the file where all the things start - main.ts. Here, in the standard fashion, our Vue instance is created and mounted. Our main <App/> component is imported from App.vue file, which we'll get back to in a second. Then, we also import the registerServiceWorker.ts file, where our PWA service worker is registered, handling the caching of our files in production mode. But, what's more important right now is our Vuex** store** setup in the store.ts file. Here, we'll store the current state of the game board, user settings and some other metadata, like user's highest score. Everything in order, in one, single place.

import Vue from 'vue';
import Vuex from 'vuex';


export default new Vuex.Store({
  state: {

  mutations: {

  actions: {


The whole main.ts file should look somewhat like this:

import Vue from 'vue';
import App from './App.vue';
import store from './store';
import './registerServiceWorker';

Vue.config.productionTip = false;

new Vue({
  render: h => h(App),

In the src folder, there are also two sub-directories - one for assets and one for components. It's always good to have some order when it comes to the files structure. The components folder should contain only .vue files, just like our App.vue. But, remember that we're talking about TypeScript here! So, what is going to change within our Vue template files?

Well, first, let's take a look at the general structure of our standard Vue template:


<script lang="ts">


The main <template/> tag remains unchanged. Our <script/> tag obtains new lang attribute, to inform the transpiler of the use of TypeScript. Finally, I decided to not go with any CSS preprocessor, so only pure CSS <style/> tag remains.

Now, in the case of Vue templates, you might be accustomed to just exporting an object with some properties and methods, like data or methods. With TypeScript, things are looking a little different...

import { Component, Vue } from 'vue-property-decorator';

export default class App extends Vue {}

As you can see, instead of an object, the primary value we export is a class extending the base Vue class. What you see above that with a nice-looking @Component syntax is a so-called decorator. It makes external modification and annotation of classes and properties possible. It's currently a stage 2 proposal of ECMAScript, leaving us to speculate if it's going to make it to the standard-grade JavaScript release one day. Of course, they are already implemented in Babel and TypeScript (although mostly in its stage-1-like version), where they gained popularity because of their use-cases in Angular.

The official @Component decorator provided by the vue-class-component package serves exactly the same purpose as demonstrated - to create class-based Vue components, mainly in TypeScript. The vue-property-decorator is the 3rd-party package, although very popular and acknowledged, that implements the @Component decorator, and provides you with 6 more! They allow you to e.g. use props just like they were class properties, instead of specifying them through the @Componentdecorator. Anyway, we'll most-likely explore most of this stuff down the road.

Also, the *class + *@Component decorator approach comes with a few benefits of its own! For example, you can declare your data properties, computed properties, and methods, just like they were properties and methods of the given class.

// ...
export default class App extends Vue {
    myProp: 10;
    get myComputedProp() {
        return this.myProp;
    myMethod() {

Notice that, instead of computed properties, you can simply use native JS getters and setters to achieve the same result, with a much nicer and denser syntax! After all, I think that the class components present a great alternative for what we've already seen. Their only downside right now is small user-base (but growing!) and thus, the general support for this solution. But, I think it's still worth a try.


Our game - the UI and WebGL content itself - should have a unified look. I decided to go with one of my favorite and very popular design system - Material Design (MD). We won't be implementing everything according to the specs - only the general look and feel. From Vue's site, we'll use Vuetify - a great library that gives you access to a lot of MD components on its own. Also, such choice will greatly simply our WebGL scene - we'll use flat lighting and add some low-poly look to our tiles, so everything will have a unified feeling.

For now, we'll only install and set up the pure basics of Vuetify. After the Three.js part will be done, we'll continue our work with UI. So, let's first install Vuetify.

yarn add vuetify

Next, let Vue know that we want to use it, by making some additions to our main.ts file.

// ...
import Vuetify from 'vuetify';
import 'vuetify/dist/vuetify.min.css';

// ...

new Vue({
  // ...

We also import the CSS using standard import syntax. Vue CLI has already set it up for us.

Vuetify also requires us to import Roboto fonts (and optionally MD icons font) in order for everything to work and be displayed correctly. For this, we'll edit the <head/> section of our public/index.html file.

<!-- ... -->
    <!-- ... -->
    <link href=",300,400,500,700,900|Material+Icons" rel="stylesheet">
<!-- ... -->

Finally, let's make some clean up in our src directory. Remove all the files we won't use and edit the App.vue to add Vuetify base markup.


Of course, remember to use yarn serve in order to see your changes in real-time.


The basic idea behind implementing Three.js in this project is to use Vue as an abstraction layer. Some of our Vue components will only serve the purpose of managing the WebGL scene. You can see a similar concept implemented on a much more advanced level with Mozilla's AFrame - Three.js-based WebVR library in which you manage your 3D scene with a set of custom HTML elements. We could use it, but it may bring some disorder to our code, with us having to keep custom AFrame elements and Vue components separate. Also, the purpose of this project is to learn something new, so...

yarn add three

We'll start by installing Three.js and creating our first <Scene/> component within the src/components directory.

  <div class="scene" ref="scene"/>

<script lang="ts">
// ...

<style scoped>
.scene {
  width: 100%;
  height: 100%;

Our <Scene/> component will contain only one, single <div/> in order to house the Three.js rendering canvas. Now, let's check the full code, shall we?

import { Component, Vue } from "vue-property-decorator";
import * as THREE from "three";

  mounted() {
    const el = this.$refs.scene as Element; = new THREE.PerspectiveCamera(
      el.clientWidth / el.clientHeight,
    this.renderer.setSize(el.clientWidth, el.clientHeight);

    const geometry = new THREE.BoxGeometry(1, 1, 1);
    const material = new THREE.MeshBasicMaterial({ color: 0x00ff00 });
    const cube = new THREE.Mesh(geometry, material);

    this.scene.add(cube); = 5;
    const animate = () => {

      cube.rotation.x += 0.01;
      cube.rotation.y += 0.01;


export default class Scene extends Vue {
  private camera!: THREE.PerspectiveCamera;
  private renderer: THREE.WebGLRenderer = new THREE.WebGLRenderer();
  private scene: THREE.Scene = new THREE.Scene();

Phew... that's quite a lot of code. Let's break it down!

What you see above is the most basic example of a green, rotating cube, taken straight from Three.js documentation. Of course, it's slightly changed and adapted to our Vue + TypeScript code-base.

First, take a look at the bottom, cause it's here where it all starts. We declare some base properties of our class. We make all of them private, as no one needs to use them besides us and we also note that the camera will be assigned later (using !).

Then, in the mounted callback, we do the basic Three.js workflow. Notice the use of generics at the very top, with a @Component decorator - it allows us to keep our code type-safe, as decorators still don't have too good support in TypeScript. Then, the mounted() hook cannot be declared inside the class itself - whether with additional decorators or not. It just has to be where it is right now - just like all the other lifecycle hooks.

The code inside mounted() hook is just accessing the scene element though reference, a setup of basic Three.js scene, together with camera and an animated cube.

After all, is done in component, we need to get back to the App.vue file and connect these two together.

import { Component, Vue } from "vue-property-decorator";
import Scene from "./components/Scene.vue";

  components: {
export default class App extends Vue {}

We import our component and use the components property in our @Component decorator's config to specify that we're using it. Then, we can safely use its markup in our template.


And... we should have our Three.js scene up and running!

It's just the beginning...

I know that the end result might not be mind-blowing, but hey! - We're just getting started! In the next post, I'm planning on creating the actual board, so, if you liked this post, stay tuned for that! Oh, and you can find all the code from this part of the tutorial here, on GitHub.

Why You Should Use TypeScript With Vue.js

Why You Should Use TypeScript With Vue.js

Why You Should Use TypeScript With Vue.js: It’s easier to write good TypeScript code than good JavaScript code

Why Use TypeScript

Statically typed languages are awesome. Consider the following example in plain JavaScript.

function getBooks(query, user) {
  let books = []
  // Retrieve books from API
  return books

This code is hard to understand for the following reasons.

  • There is no way to know what type the query and user parameters need to be. The user parameter could be a user object or a string denoting the user’s name — or something else, for that matter.
  • There is no way to know what data type the function returns. It returns an array, but is it an array of strings, objects, or book ids?

The only way to communicate this in plain JavaScript is via comments.

Let’s rewrite this code using TypeScript.

class Book {
  id!: number
  user_id!: number

  title: string

function getBooks(query: string, user: number): Book[] {
    let books : Book[] = [];

    // Retrieve books from API

    return books;


First, I define a class Book, with each book having an id, a user_id, and a title.

Next, I specify that the query parameter is a string and the user parameter a number. The function returns an array of books.

Because I specified this, my IDE can do some amazing autocompletes.

Hopefully, you are convinced that TypeScript is amazing. Let's dive into how to set up a Vue TypeScript project!

Create the Vue Project

To create the project, I’m going to use the Vue CLI. This is the official command-line interface that makes creating a new Vue project a whole lot easier.

npm install -g @vue/cli

I can now create the project with a simple command.

vue create my-project-name

Make sure to select “Manually select features” in the prompt.

For this project, I will only use the TypeScript and Router features.

Class style component syntax

When asked if I want to use class-style component syntax, I select yes.

Normally each Vue component will export a JavaScript object, like so:

  export default {
    props: {
      msg: String,
    data() { 
      return {
        title: "Hello World!",

However, when using the class style component syntax, each Vue component will export a class instead of an object. The HelloWorld component would then look like this:

<script lang="ts">
  import { Component, Prop, Vue } from 'vue-property-decorator';
  export default class HelloWorld extends Vue {
    msg!: string
    title:string = "Hello World"

Personally, I find this syntax much cleaner.

View Your Vue App

Select the default values for all other settings. After the setup wizard is done, run npm run serve to start the development server. Head over to localhost:8080 and view the Vue app!

That’s all there is to it. Thanks to the Vue CLI, everything works out of the box. I still have nightmares about debugging my Webpack config file, so the CLI is a genuine lifesaver.

How to use TypeScript with Vue.js

How to use TypeScript with Vue.js

In this artilce, you'll learn how to use TypeScript with Vue.js

inb4: This is not another “setting up” a new project with Vue and TypeScript tutorial. Let’s do some deep dive into more complex topics!

typescript is awesome. Vue is awesome. No doubt, that a lot of people try to bundle them together. But, due to different reasons, it is hard to really type your Vue app. Let’s find out what are the problems and what can be done to solve them (or at least minimize the impact).


We have this wonderful template with Nuxt, Vue, Vuex, and jest fully typed. Just install it and everything will be covered for you. Go to the docs to learn more.

And as I said I am not going to guide you through the basic setup for three reasons:

  1. There are a lot of existing tutorials about it
  2. There are a lot of tools to get started with a single click like Nuxt and vue-cli with typescript plugin
  3. We already have [wemake-vue-template]( where every bit of setup that I going to talk about is already covered
Component typings

The first broken expectation when you start to work with Vue and typescript and after you have already typed your class components is that `` and `

I have made two typos here: `{{ usr }}` instead of `{{ user }}` and `$style.headr` instead of `$style.header`. Will `typescript` save me from these errors? Nope, it won’t.

What can be done to fix it? Well, there are several hacks.

### Typing the template

One can use `Vetur` with `vetur.experimental.templateInterpolationService` option to type-check your templates. Yes, this is only an editor-based check and it cannot be used inside the CI yet. But, `Vetur` team is working hard to provide a [CLI]( to allow this. Track [the original issue]( in case you are interested.

![TypeScript with Vue.js](

The second option is two write snapshot tests with `jest`. It will catch a lot of template-based errors. And it is quite cheap in the maintenance.

So, the combination of these two tools provides you a nice Developer Experience with fast feedback and a reliable way to catch errors inside the CI.

### Typing styles

Typing `css-module`s is also covered by several external tools:

*   [typings-for-css-modules-loader](
*   [css-modules-typescript-loader](

The main idea of these tools is to fetch `css-module`s and then create `.d.ts` declaration files out of them. Then your styles will be fully typed. It is still not implemented for `Nuxt` or `Vue`, but you can tract [this issue]( for progress.

However, I don’t personally use any of these tools in my projects. They might be useful for projects with large code bases and a lot of styles, but I am fine with just snapshots.

Styleguides with visual regression tests also help a lot. `[@storybook/addon-storyshots](` is a nice example of this technique.

## [Vuex](

The next big thing is `Vuex`. It has some built-in by-design complexity for typing:

const result: Promise = this.$store.dispatch('action_name', { payload: 1 })

The problem is that `'action_name'` might no exist, take other arguments, or return a different type. That’s not something you expect for a fully-typed app.

What are the existing solutions?

### vuex-class

`[vuex-class](` is a set of decorators to allow easy access from your class-based components to the `Vuex` internals.

But, it [is not typed safe]( since it cannot interfere with the types of state, getters, mutations, and actions.

![TypeScript with Vue.js](

Of course, you can manually annotate types of properties.

![TypeScript with Vue.js](

But what are you going to do when the real type of your state, getters, mutations, or actions will change? You will have a hidden type mismatch.

### vuex-simple

That’s where `vuex-simple` helps us. It actually offers a completely different way to write your `Vuex` code and that’s what makes it type safe. Let’s have a look:

import { Action, Mutation, State, Getter } from 'vuex-simple'

class MyStore {

// State

public comments: CommentType[] = []

// Getters

public get hasComments (): boolean {
return Boolean(this.comments && this.comments.length > 0)

// Mutations

public setComments (payload: CommentType[]): void {
this.comments = updatedComments

// Actions

public async fetchComments (): Promise {
// Calling some API:
const commentsList = await api.fetchComments()
this.setComments(commentsList) // typed mutation
return commentsList

Later this typed module can be registered inside your `Vuex` like so:

import Vue from 'vue'
import Vuex from 'vuex'
import { createVuexStore } from 'vuex-simple'

import { MyStore } from './store'


// Creates our typed module instance:
const instance = new MyStore()

// Returns valid Vuex.Store instance:
export default createVuexStore(instance)

Now we have a 100% native `Vuex.Store` instance and all the type information bundled with it. To use this typed store in the component we can write just one line of code:

import Vue from 'vue'
import Component from 'nuxt-class-component'
import { useStore } from 'vuex-simple'

import MyStore from './store'

export default class MyComponent extends Vue {
// That's all we need!
typedStore: MyStore = useStore(this.$store)

// Demo: will be typed as Comment[]:
comments = typedStore.comments

Now we have typed `Vuex` that can be safely used inside our project. When we change something inside our store definition it is automatically reflected to the components that use this store. If something fails - we know it as soon as possible.

There are also different libraries that do the same but have different API. Choose what suits you best.

## [API calls](

When we have `Vuex` correctly setup, we need to fill it with data. Let’s have a look at our action definition again:

public async fetchComments (): Promise {
// Calling some API:
const commentsList = await api.fetchComments()
// ...
return commentsList

How can we know that it will really return a list of `CommentType` and not a single `number` or a bunch of `AuthorType` instances?

We cannot control the server. And the server might actually break the contract. Or we can simply pass the wrong `api` instance, make a typo in the URL, or whatever.

How can we be safe? We can use runtime typing! Let me introduce `[io-ts](` to you:

import * as ts from 'io-ts'

export const Comment = ts.type({
'id': ts.number,
'body': ts.string,
'email': ts.string,

// Static TypeScript type, that can be used as a regular type:
export type CommentType = ts.TypeOf

What do we do here?

1.  We define an instance of `ts.type` with fields that we need to be checked in runtime when we receive a response from server
2.  We define a static type to be used in annotation without any extra boilerplate

And later we can use it our `api` calls:

import * as ts from 'io-ts'
import * as tPromise from 'io-ts-promise'

public async fetchComments (): Promise {
const response = await axios.get('comments')
return tPromise.decode(ts.array(Comment),

With the help of `[io-ts-promise](`, we can return a `Promise` in a failed state if the response from server does not match a `ts.array(Comment)` type. It really works like a validation.

.then((data) => /* ... /
Happens with both request failure and incorrect response type */)

Moreover, return type annotation is in sync with the `.decode` method. And you cannot put random nonsense there:

![How to use TypeScript](

With the combination of runtime and static checks, we can be sure that our requests won’t fail because of the type mismatch. But, to be 100% sure that everything works, I would recommend using contract-based testing: have a look at `[pact](` as an example. And monitor your app with `Sentry`.

## [Vue Router](

The next problem is that `this.$router.push({ name: 'wrong!' })` does not work the way we want to.

I would say that it would be ideal to be warned by the compiler that we are routing to the wrong direction and this route does not exist. But, it is not possible. And not much can be done: there are a lot of dynamic routes, regex, fallbacks, permissions, etc that can eventually break. The only option is to test each `this.$router` call in your app.

## vue-test-utils

Speaking about tests I do not have any excuses not to mention `@vue/test-utils` that also has some problems with typing.

When we will try to test our new shiny component with `typedStore` property, we will find out that we actually cannot do that according to the `typescript`:

![How to use TypeScript](

Why does this happen? It happens because `mount()` call does not know anything about your component’s type, because all components have a `VueConstructor` type by default:

![How to use TypeScript](

That’s where all the problems come from. What can be done? You can use `[vuetype](` to produce `YouComponent.vue.d.ts` typings that will tell your tests the exact type of the mounted component.

You can also track [this issue]( for the progress.

But, I don’t like this idea. These are tests, they can fail. No big deal. That’s why I stick to `(wrapper.vm as any).whatever` approach. This saves me quite a lot of time to write tests. But spoils Developer Experience a little bit.

Make your own decision here:

*   Use `vuetype` all the way
*   Partially apply it to the most important components with the biggest amount of tests and update it regularly
*   Use `any` as a fallback

## Conclusion

The average level of `typescript` support in `Vue` ecosystem increased over the last couple of years:

*   `Nuxt` firstly introduced `nuxt-ts` and now ships `ts` builds by default
*   `[email protected]` will have improved `typescript` support
*   More 3rd-party apps and plugins will provide type definitions

But, it is production ready at the moment. These are just things to improve! Writing type-safe `Vue` code really improves your Developer Experience and allows you to focus on the important stuff while leaving the heavy-lifting to the compiler.

What are your favorite hacks and tools to type `Vue` apps? Let’s discuss it in the comment section.