Adding TypeScript Support to Quasar

After spending 2 years with Polymer and another with Angular 2, I have recently started rediscovered my love for Vue.js.

Quasar is a very nice Ionic-like mobile framework for Vue, and since Angular 2 brainwashed me into using TypeScript, I decided to go thru the process of integrating it into a Quasar app. Here’s 11 simple steps to adding TypeScript support.

Step 1: Create app

Assuming that you’ve installed it, we are just going to follow the steps using Quasar CLI.

Step 2: Install TypeScript dependencies

We need a few dependencies to build our TypeScript project.

npm install --save-dev ts-loader typescript @types/node

We will also need the Vue.js class decorators for TypeScript/ES2015.

npm install --save vue-class-component

Step 3: Add your tsconfig.json file

{
"compilerOptions": {
"target": "es5",
"lib": [
"dom",
"es2015"
],
"types": [
"node"
],
"module": "commonjs",
"moduleResolution": "node",
"isolatedModules": false,
"experimentalDecorators": true,
"noImplicitThis": true,
"strictNullChecks": true,
"removeComments": true,
"suppressImplicitAnyIndexErrors": true
},
"include": [
"./src/**/*.ts"
],
"compileOnSave": false
}

Step 4: Update webpack.base.conf.js

var
path = require('path'),
webpack = require('webpack'),
config = require('../config'),
cssUtils = require('./css-utils'),
env = require('./env-utils'),
merge = require('webpack-merge'),
projectRoot = path.resolve(__dirname, '../'),
ProgressBarPlugin = require('progress-bar-webpack-plugin'),
useCssSourceMap =
(env.dev && config.dev.cssSourceMap) ||
(env.prod && config.build.productionSourceMap)

module.exports = {
entry: {
app: './src/main.ts'
},
output: {
path: path.resolve(__dirname, '../dist'),
publicPath: config[env.prod ? 'build' : 'dev'].publicPath,
filename: 'js/[name].js',
chunkFilename: 'js/[id].[chunkhash].js'
},
resolve: {
extensions: ['.ts', '.js'],
modules: [
path.join(__dirname, '../src'),
'node_modules'
],
alias: config.aliases
},
module: {
rules: [
{
test: /\.ts$/,
exclude: /node_modules|vue\/src/,
loader: 'ts-loader',
options: {
appendTsSuffixTo: [/\.vue$/]
}
},
{
test: /\.vue$/,
loader: 'vue-loader',
options: {
esModule: true,
postcss: cssUtils.postcss,
loaders: merge(cssUtils.styleLoaders({
sourceMap: useCssSourceMap,
extract: env.prod
}))
}
},
{
test: /\.json$/,
loader: 'json-loader'
},
{
test: /\.(png|jpe?g|gif|svg)(\?.*)?$/,
loader: 'url-loader',
options: {
limit: 10000,
name: 'img/[name].[hash:7].[ext]'
}
},
{
test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/,
loader: 'url-loader',
options: {
limit: 10000,
name: 'fonts/[name].[hash:7].[ext]'
}
}
]
},
plugins: [
/* Uncomment if you wish to load only one Moment locale: */
// new webpack.ContextReplacementPlugin(/moment[\/\\]locale$/, /en/),

new webpack.DefinePlugin({
'process.env': config[env.prod ? 'build' : 'dev'].env,
'DEV': env.dev,
'PROD': env.prod,
'__THEME': '"' + env.platform.theme + '"'
}),
new webpack.LoaderOptionsPlugin({
minimize: env.prod,
options: {
context: path.resolve(__dirname, '../src'),
eslint: {
formatter: require('eslint-friendly-formatter')
},
postcss: cssUtils.postcss
}
}),
new ProgressBarPlugin({
format: config.progressFormat
})
],
performance: {
hints: false
},
devtool: 'source-map'
}

The major thing that we’ve done here is swapped out the babel loader for the typescript one and adding ts to the extensions. Everything else can stay the same.

Step 5: Add a sfc.d.ts file to your src folder

declare module "*.vue" {
import * as Vue from 'vue';
export default typeof Vue
}

declare module "quasar"
declare const __THEME

This should look familiar to anyone who has seen Vue’s TypeScript example, but we have a couple additions. The first declare is so that the TypeScript compiler won’t barf when we load *.vue modules in our *.ts modules. The second declare is to make up for the lack of Quasar type declarations. The last declare statement is a global constant Quasar uses to assign your theme. It is injected at build time in the webpack config.

Step 6: Rename *.js modules to *.ts

Simple enough, we just rename all of the JavaScript files in our src directory as TypeScript files.

Step 7: Fix Vue Imports & Modules

import Vue from 'vue'
import VueRouter from 'vue-router'

Becomes

import * as Vue from 'vue'
import * as VueRouter from 'vue-router'

Also it’s important to note that we will do the same for all of our Quasar import statements.

Step 8: Update Vue components to use class decorator

Replace the script in Index.vue with the following

var moveForce = 30
var rotateForce = 40

import * as Quasar from 'quasar'
import * as Vue from 'vue'
import Component from 'vue-class-component'

@Component({
props: {
propMessage: String
}
})
export default class Index extends Vue {
quasarVersion = Quasar.version
moveX = 0
moveY = 0
rotateY = 0
rotateX = 0
get position() {

let transform = `rotateX(${this.rotateX}deg) rotateY(${this.rotateY}deg)`
return {
top: this.moveY + 'px',
left: this.moveX + 'px',
'-webkit-transform': transform,
'-ms-transform': transform,
transform
}
}

move(event) {
const {width, height} = Quasar.Utils.dom.viewport()
const {top, left} = Quasar.Utils.event.position(event)
const halfH = height / 2
const halfW = width / 2

this.moveX = (left - halfW) / halfW * -moveForce
this.moveY = (top - halfH) / halfH * -moveForce
this.rotateY = (left / width * rotateForce * 2) - rotateForce
this.rotateX = -((top / height * rotateForce * 2) - rotateForce)
}
mounted() {
this.$nextTick(() => {
document.addEventListener('mousemove', this.move)
document.addEventListener('touchmove', this.move)
})
}
beforeDestroy() {
document.removeEventListener('mousemove', this.move)
document.removeEventListener('touchmove', this.move)
}
}

Also, replace the script in Error404.vue

var Quasar = require('quasar')
import * as Vue from 'vue'
import Component from 'vue-class-component'

@Component({})
export default class ErrorComponent extends Vue {
canGoBack = window.history.length > 1
goBack() {
window.history.go(-1)
}
data() {
return {
canGoBack: window.history.length > 1
}
}
}

The big difference here is we now use classes that are wrapped with the component decorator. This is very similar to Angular 2’s syntax. You can read more about why we do this here.

Step 9: Replace System loader in router.ts

I prefer to directly import my components but you can also use require here.

import * as Vue from 'vue'
import * as VueRouter from 'vue-router'
import HomeComponent from './components/Index.vue'
import ErrorComponent from './components/Error404.vue'

Vue.use(VueRouter)

export const AppRouter = new VueRouter({
routes: [
{ path: '/', component: HomeComponent }, // Default
{ path: '/login', component: LoginComponent }, // Default
{ path: '*', component: ErrorComponent } // Not found
]
})

export default AppRouter

Step 10: Update main.ts

import * as Vue from 'vue'
import * as Quasar from 'quasar'

import router from './router'
import app from './App.vue';

Vue.use(Quasar) // Install Quasar Framework

Quasar.start(() => {
/* eslint-disable no-new */
new Vue({
el: '#q-app',
router,
render: h => h(app)
})
})

Step 11: Cleanup & Build Awesome!

That’s everything. We should now be able to run quasar dev to start our server, and we now have the tooling support of TypeScript. Any dependencies that we no longer need can be safely removed. Happy programming!