diff --git a/.babelrc b/.babelrc
index 9143cd8d1..a64b362c2 100644
--- a/.babelrc
+++ b/.babelrc
@@ -3,6 +3,6 @@
     [ "env", {"modules": false} ],
     "stage-2"
   ],
-  "plugins": ["transform-runtime"],
+  "plugins": ["transform-runtime", "transform-decorators"],
   "comments": false
 }
diff --git a/.eslintignore b/.eslintignore
index 9b1c8b133..25aeecd2a 100644
--- a/.eslintignore
+++ b/.eslintignore
@@ -1 +1,2 @@
 /dist
+*.ts
diff --git a/package-lock.json b/package-lock.json
index da0a3a614..3e5e2a852 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -2747,6 +2747,12 @@
       "integrity": "sha1-ogM8CcyOFY03dI+951B4Mr1s4Sc=",
       "dev": true
     },
+    "diff": {
+      "version": "3.4.0",
+      "resolved": "https://registry.npmjs.org/diff/-/diff-3.4.0.tgz",
+      "integrity": "sha512-QpVuMTEoJMF7cKzi6bvWhRulU1fZqZnvyVQgNhPaxxuTYwyjn/j1v9falseQ/uXWwPnO56RBfwtg4h/EQXmucA==",
+      "dev": true
+    },
     "diffie-hellman": {
       "version": "5.0.2",
       "resolved": "https://registry.npmjs.org/diffie-hellman/-/diffie-hellman-5.0.2.tgz",
@@ -10167,6 +10173,112 @@
         "semver": "5.5.0"
       }
     },
+    "tslib": {
+      "version": "1.9.0",
+      "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.9.0.tgz",
+      "integrity": "sha512-f/qGG2tUkrISBlQZEjEqoZ3B2+npJjIf04H1wuAv9iA8i04Icp+61KRXxFdha22670NJopsZCIjhC3SnjPRKrQ==",
+      "dev": true
+    },
+    "tslint": {
+      "version": "5.9.1",
+      "resolved": "https://registry.npmjs.org/tslint/-/tslint-5.9.1.tgz",
+      "integrity": "sha1-ElX4ej/1frCw4fDmEKi0dIBGya4=",
+      "dev": true,
+      "requires": {
+        "babel-code-frame": "6.26.0",
+        "builtin-modules": "1.1.1",
+        "chalk": "2.3.1",
+        "commander": "2.14.1",
+        "diff": "3.4.0",
+        "glob": "7.1.2",
+        "js-yaml": "3.10.0",
+        "minimatch": "3.0.4",
+        "resolve": "1.5.0",
+        "semver": "5.5.0",
+        "tslib": "1.9.0",
+        "tsutils": "2.21.2"
+      }
+    },
+    "tslint-config-airbnb": {
+      "version": "5.7.0",
+      "resolved": "https://registry.npmjs.org/tslint-config-airbnb/-/tslint-config-airbnb-5.7.0.tgz",
+      "integrity": "sha1-Cf8EsN1Zl2X0S0QlAERY0I/LLEA=",
+      "dev": true,
+      "requires": {
+        "tslint-consistent-codestyle": "1.11.1",
+        "tslint-eslint-rules": "4.1.1",
+        "tslint-microsoft-contrib": "5.0.3"
+      }
+    },
+    "tslint-consistent-codestyle": {
+      "version": "1.11.1",
+      "resolved": "https://registry.npmjs.org/tslint-consistent-codestyle/-/tslint-consistent-codestyle-1.11.1.tgz",
+      "integrity": "sha512-wLu+Ct8x4mBmVkuhEiNAnUBkxchMV2Le0ikBsST5HnKbGlm3K4RSpXCBSI1VtJDk748W2I5hDzgsInawLdnxwQ==",
+      "dev": true,
+      "requires": {
+        "tslib": "1.9.0",
+        "tsutils": "2.21.2"
+      }
+    },
+    "tslint-eslint-rules": {
+      "version": "4.1.1",
+      "resolved": "https://registry.npmjs.org/tslint-eslint-rules/-/tslint-eslint-rules-4.1.1.tgz",
+      "integrity": "sha1-fDDniC8mvCdr/5HSOEl1xp2viLo=",
+      "dev": true,
+      "requires": {
+        "doctrine": "0.7.2",
+        "tslib": "1.9.0",
+        "tsutils": "1.9.1"
+      },
+      "dependencies": {
+        "doctrine": {
+          "version": "0.7.2",
+          "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-0.7.2.tgz",
+          "integrity": "sha1-fLhgNZujvpDgQLJrcpzkv6ZUxSM=",
+          "dev": true,
+          "requires": {
+            "esutils": "1.1.6",
+            "isarray": "0.0.1"
+          }
+        },
+        "esutils": {
+          "version": "1.1.6",
+          "resolved": "https://registry.npmjs.org/esutils/-/esutils-1.1.6.tgz",
+          "integrity": "sha1-wBzKqa5LiXxtDD4hCuUvPHqEQ3U=",
+          "dev": true
+        },
+        "isarray": {
+          "version": "0.0.1",
+          "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz",
+          "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=",
+          "dev": true
+        },
+        "tsutils": {
+          "version": "1.9.1",
+          "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-1.9.1.tgz",
+          "integrity": "sha1-ufmrROVa+WgYMdXyjQrur1x1DLA=",
+          "dev": true
+        }
+      }
+    },
+    "tslint-microsoft-contrib": {
+      "version": "5.0.3",
+      "resolved": "https://registry.npmjs.org/tslint-microsoft-contrib/-/tslint-microsoft-contrib-5.0.3.tgz",
+      "integrity": "sha512-5AnfTGlfpUzpRHLmoojPBKFTTmbjnwgdaTHMdllausa4GBPya5u36i9ddrTX4PhetGZvd4JUYIpAmgHqVnsctg==",
+      "dev": true,
+      "requires": {
+        "tsutils": "2.21.2"
+      }
+    },
+    "tsutils": {
+      "version": "2.21.2",
+      "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-2.21.2.tgz",
+      "integrity": "sha512-iaIuyjIUeFLdD39MYdzqBuY7Zv6+uGxSwRH4mf+HuzsnznjFz0R2tGrAe0/JvtNh91WrN8UN/DZRFTZNDuVekA==",
+      "dev": true,
+      "requires": {
+        "tslib": "1.9.0"
+      }
+    },
     "tty-browserify": {
       "version": "0.0.0",
       "resolved": "https://registry.npmjs.org/tty-browserify/-/tty-browserify-0.0.0.tgz",
@@ -10711,6 +10823,11 @@
       "integrity": "sha512-3D+lY7HTkKbtswDM4BBHgqyq+qo8IAEE8lz8va1dz3LLmttjgo0FxairO4r1iN2OBqk8o1FyL4hvzzTFEdQSEw==",
       "dev": true
     },
+    "vue-class-component": {
+      "version": "6.2.0",
+      "resolved": "https://registry.npmjs.org/vue-class-component/-/vue-class-component-6.2.0.tgz",
+      "integrity": "sha512-U11yVeP5zjPSx4IU7Zas3MLC+Vy9dmufI+uLKLo8YuGQJGOihSYfh/fgNnbjMteN+hz5axjG6iC6ybMo6vGYnA=="
+    },
     "vue-eslint-parser": {
       "version": "2.0.2",
       "resolved": "https://registry.npmjs.org/vue-eslint-parser/-/vue-eslint-parser-2.0.2.tgz",
diff --git a/package.json b/package.json
index 123d2d521..6341e6902 100644
--- a/package.json
+++ b/package.json
@@ -7,16 +7,20 @@
   "author": "BrewPi B.V.",
   "private": true,
   "scripts": {
-    "lint": "eslint --ext .js,.vue,.ts src",
+    "eslint": "eslint --ext .js,.vue src",
+    "tslint": "tslint -c tslint.json 'src/**/*.ts'",
+    "lint": "npm run eslint && npm run tslint",
     "test": "echo \"No test specified\" && exit 0"
   },
   "dependencies": {
+    "vue-class-component": "^6.2.0",
     "vue-i18n": "^7.3.3",
     "vuex-typescript": "^3.0.2"
   },
   "devDependencies": {
     "@types/core-js": "^0.9.46",
     "babel-eslint": "8.2.1",
+    "babel-plugin-transform-decorators": "^6.24.1",
     "connect-api-mocker": "^1.3.6",
     "eslint": "4.15.0",
     "eslint-config-airbnb-base": "11.3.0",
@@ -27,6 +31,8 @@
     "eslint-plugin-vue": "4.0.0",
     "quasar-cli": "^0.15.0-beta.36",
     "ts-loader": "^3.5.0",
+    "tslint": "^5.9.1",
+    "tslint-config-airbnb": "^5.7.0",
     "typescript": "^2.7.1",
     "typescript-eslint-parser": "^13.0.0"
   },
diff --git a/quasar.conf.js b/quasar.conf.js
index 022dba303..c5b769fd3 100644
--- a/quasar.conf.js
+++ b/quasar.conf.js
@@ -36,17 +36,17 @@ module.exports = ctx => ({
 
       // add custom loaders
       cfg.module.rules.push({
-        enforce: 'pre',
-        test: /\.(js|vue)$/,
-        loader: 'eslint-loader',
-        exclude: /(node_modules|quasar)/,
-      }, {
         test: /\.tsx?$/,
         loader: 'ts-loader',
         exclude: /node_modules/,
         options: {
           appendTsSuffixTo: [/\.vue$/],
         },
+      }, {
+        enforce: 'pre',
+        test: /\.(js|vue)$/,
+        loader: 'eslint-loader',
+        exclude: /(node_modules|quasar)/,
       });
     },
   },
diff --git a/src/components/blocks/OneWireTempSensor/OneWireTempSensor.ts b/src/components/blocks/OneWireTempSensor/OneWireTempSensor.ts
new file mode 100644
index 000000000..4e6c8ff1e
--- /dev/null
+++ b/src/components/blocks/OneWireTempSensor/OneWireTempSensor.ts
@@ -0,0 +1,26 @@
+import Vue from 'vue';
+import Component from 'vue-class-component';
+
+import { getById } from '../../../store/blocks/OneWireTempSensor/getters';
+
+@Component({
+  props: {
+    id: {
+      default: '',
+      type: String,
+    },
+  },
+})
+export default class OneWireTempSensor extends Vue {
+  get blockData() {
+    return getById(this.$props.id);
+  }
+
+  get settings() {
+    return this.blockData.settings;
+  }
+
+  get state() {
+    return this.blockData.state;
+  }
+}
diff --git a/src/components/blocks/OneWireTempSensor.vue b/src/components/blocks/OneWireTempSensor/default.vue
similarity index 72%
rename from src/components/blocks/OneWireTempSensor.vue
rename to src/components/blocks/OneWireTempSensor/default.vue
index e85ca8218..373209bd9 100644
--- a/src/components/blocks/OneWireTempSensor.vue
+++ b/src/components/blocks/OneWireTempSensor/default.vue
@@ -26,24 +26,7 @@
   </q-card>
 </template>
 
-<script>
-import Vue from 'vue';
-
-export default Vue.extend({
-  name: 'one-wire-temp-sensor',
-  props: {
-    id: String,
-    settings: {
-      address: String,
-      offset: Number,
-    },
-    state: {
-      value: Number,
-      connected: Boolean,
-    },
-  },
-});
-</script>
+<script lang="ts" src="./OneWireTempSensor.ts" />
 
 <style scoped>
 
diff --git a/src/components/blocks/SetPointSimple/SetPointSimple.ts b/src/components/blocks/SetPointSimple/SetPointSimple.ts
new file mode 100644
index 000000000..892fa1c6d
--- /dev/null
+++ b/src/components/blocks/SetPointSimple/SetPointSimple.ts
@@ -0,0 +1,22 @@
+import Vue from 'vue';
+import Component from 'vue-class-component';
+
+import { getById } from '../../../store/blocks/SetPointSimple/getters';
+
+@Component({
+  props: {
+    id: {
+      default: '',
+      type: String,
+    },
+  },
+})
+export default class SetPointSimple extends Vue {
+  get blockData() {
+    return getById(this.$props.id);
+  }
+
+  get settings() {
+    return this.blockData.settings;
+  }
+}
diff --git a/src/components/blocks/SetPointSimple.vue b/src/components/blocks/SetPointSimple/default.vue
similarity index 59%
rename from src/components/blocks/SetPointSimple.vue
rename to src/components/blocks/SetPointSimple/default.vue
index d12e34bdd..09dce042e 100644
--- a/src/components/blocks/SetPointSimple.vue
+++ b/src/components/blocks/SetPointSimple/default.vue
@@ -11,20 +11,4 @@
   </q-card>
 </template>
 
-<script>
-import Vue from 'vue';
-
-export default Vue.extend({
-  name: 'set-point-simple',
-  props: {
-    id: String,
-    settings: {
-      value: Number,
-    },
-  },
-});
-</script>
-
-<style scoped>
-
-</style>
+<script lang="ts" src="./SetPointSimple.ts"></script>
diff --git a/src/components/blocks/block.ts b/src/components/blocks/block.ts
deleted file mode 100644
index b6a920332..000000000
--- a/src/components/blocks/block.ts
+++ /dev/null
@@ -1,24 +0,0 @@
-import Vue from 'vue';
-
-import SetPointSimple from './SetPointSimple.vue';
-import OneWireTempSensor from './OneWireTempSensor.vue';
-
-export default Vue.extend({
-  name: 'block',
-  props: ['block-data'],
-  render(createElement) {
-    const { type } = this.$props.blockData;
-    const options = {
-      props: this.$props.blockData,
-    };
-
-    switch (type) {
-      case 'OneWireTempSensor':
-        return createElement(OneWireTempSensor, options);
-      case 'SetPointSimple':
-        return createElement(SetPointSimple, options);
-      default:
-        throw new Error(`'${type}' is not a valid block type`);
-    }
-  },
-});
diff --git a/src/components/blocks/block.vue b/src/components/blocks/block.vue
new file mode 100644
index 000000000..1ab09242e
--- /dev/null
+++ b/src/components/blocks/block.vue
@@ -0,0 +1,39 @@
+<template>
+  <component
+    :is="type"
+    :id="blockId"
+  />
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+
+import { blockById } from '../../store/blocks/getters';
+
+const blockTypes = {
+  OneWireTempSensor: () => import('./OneWireTempSensor/default.vue'),
+  SetPointSimple: () => import('./SetPointSimple/default.vue'),
+};
+
+export default Vue.extend({
+  name: 'block',
+  components: { ...blockTypes },
+  props: {
+    blockId: {
+      default: '',
+      type: String,
+    },
+  },
+  computed: {
+    type(): string {
+      const type = blockById(this.$props.blockId).type;
+
+      if (Object.keys(blockTypes).indexOf(type) === -1) {
+        throw new Error(`'${type}' is not a valid block type`);
+      }
+
+      return type;
+    },
+  },
+});
+</script>
diff --git a/src/main.ts b/src/main.ts
index 59d692c5b..229a718bd 100644
--- a/src/main.ts
+++ b/src/main.ts
@@ -71,9 +71,9 @@ Vue.use(Quasar, {
 });
 
 const app = new Vue({
-  el: '#q-app',
   router,
   store,
+  el: '#q-app',
   render: h => h(App),
 });
 
diff --git a/src/pages/blocks.vue b/src/pages/blocks.vue
index f76f3cf02..d10a5f7ff 100644
--- a/src/pages/blocks.vue
+++ b/src/pages/blocks.vue
@@ -10,8 +10,8 @@
     <template v-if="blocks.length > 0">
       <block
         v-for="block in blocks"
-        :key="block.id"
-        :block-data="block"
+        :key="block"
+        :block-id="block"
       />
     </template>
   </q-page>
@@ -25,13 +25,13 @@ import Vue from 'vue';
 
 import Block from '../components/blocks/block';
 
-import { isFetching, allBlocks } from '../store/blocks/getters';
+import { isFetching, blockIds } from '../store/blocks/getters';
 
 export default Vue.extend({
   name: 'PageIndex',
   components: { Block },
   computed: {
-    blocks: () => allBlocks(),
+    blocks: () => blockIds(),
     fetching: () => isFetching(),
   },
   methods: {},
diff --git a/src/plugins/i18n.ts b/src/plugins/i18n.ts
index cffd90207..c75a9afee 100644
--- a/src/plugins/i18n.ts
+++ b/src/plugins/i18n.ts
@@ -7,8 +7,8 @@ export default ({ app, store, Vue }: PluginArguments) => {
   // Set i18n instance on app
   // This way we can use it in middleware and pages asyncData/fetch
   app.i18n = new VueI18n({
+    messages,
     locale: store.state.locale,
     fallbackLocale: 'en',
-    messages,
   });
 };
diff --git a/src/router/index.ts b/src/router/index.ts
index 4f18272ff..8a7433b3f 100644
--- a/src/router/index.ts
+++ b/src/router/index.ts
@@ -5,11 +5,11 @@ import routes from './routes';
 
 Vue.use(VueRouter);
 
-const Router = new VueRouter({
+const router = new VueRouter({
+  routes,
   // Leave as is and change from quasar.conf.js instead!
   mode: process.env.VUE_ROUTER_MODE || 'history',
   base: process.env.VUE_ROUTER_BASE || '',
-  routes,
 });
 
 /*
@@ -23,4 +23,4 @@ Router.beforeEach((to, from, next) => {
 })
 */
 
-export default Router;
+export default router;
diff --git a/src/store/blocks/OneWireTempSensor/OneWireTempSensor.d.ts b/src/store/blocks/OneWireTempSensor/OneWireTempSensor.d.ts
index db26f0870..d52b64016 100644
--- a/src/store/blocks/OneWireTempSensor/OneWireTempSensor.d.ts
+++ b/src/store/blocks/OneWireTempSensor/OneWireTempSensor.d.ts
@@ -4,11 +4,11 @@ export interface OneWireTempSensor extends BlockBase {
   settings: {
     address: string,
     offset: number,
-  },
+  };
   state: {
     value: number,
     connected: boolean,
-  },
+  };
 }
 
 export interface OneWireTempSensorBlock extends OneWireTempSensor {
diff --git a/src/store/blocks/OneWireTempSensor/getters.ts b/src/store/blocks/OneWireTempSensor/getters.ts
new file mode 100644
index 000000000..cc010d7a9
--- /dev/null
+++ b/src/store/blocks/OneWireTempSensor/getters.ts
@@ -0,0 +1,14 @@
+import { blockById } from '../getters';
+
+import { OneWireTempSensorBlock } from './OneWireTempSensor';
+
+export function getById(id: string): OneWireTempSensorBlock {
+  const block = blockById(id);
+
+  // force block type
+  if (block.type !== 'OneWireTempSensor') {
+    throw new Error('Block is not a valid OneWireTempSensor');
+  }
+
+  return block;
+}
diff --git a/src/store/blocks/SetPointSimple/SetPointSimple.d.ts b/src/store/blocks/SetPointSimple/SetPointSimple.d.ts
index 0137c25c3..2009036c1 100644
--- a/src/store/blocks/SetPointSimple/SetPointSimple.d.ts
+++ b/src/store/blocks/SetPointSimple/SetPointSimple.d.ts
@@ -3,7 +3,7 @@ import { BlockBase } from '../state';
 export interface SetPointSimple extends BlockBase {
   settings: {
     value: number,
-  },
+  };
 }
 
 export interface SetPointSimpleBlock extends SetPointSimple {
diff --git a/src/store/blocks/SetPointSimple/getters.ts b/src/store/blocks/SetPointSimple/getters.ts
new file mode 100644
index 000000000..14724b73a
--- /dev/null
+++ b/src/store/blocks/SetPointSimple/getters.ts
@@ -0,0 +1,14 @@
+import { blockById } from '../getters';
+
+import { SetPointSimpleBlock } from './SetPointSimple';
+
+export function getById(id: string): SetPointSimpleBlock {
+  const block = blockById(id);
+
+  // force block type
+  if (block.type !== 'SetPointSimple') {
+    throw new Error('Block is not a valid SetPointSimple');
+  }
+
+  return block;
+}
diff --git a/src/store/blocks/getters.ts b/src/store/blocks/getters.ts
index b1e042c60..e1578861b 100644
--- a/src/store/blocks/getters.ts
+++ b/src/store/blocks/getters.ts
@@ -7,6 +7,10 @@ import { State as RootState } from '../state';
 const { read } = getStoreAccessors<BlocksState, RootState>('blocks');
 
 const getters = {
+  blocksById: (state: BlocksState): { [id: string]: Block } => state.byId,
+  blockIds(state: BlocksState): string[] {
+    return state.allIds;
+  },
   allBlocks(state: BlocksState): Block[] {
     return state.allIds.map(id => state.byId[id]);
   },
@@ -17,8 +21,12 @@ const getters = {
 
 const readIsFetching = read(getters.isFetching);
 const readAllBlocks = read(getters.allBlocks);
+const readBlockIds = read(getters.blockIds);
+const readBlocksById = read(getters.blocksById);
 
 export const allBlocks = () => readAllBlocks(store);
+export const blockIds = () => readBlockIds(store);
+export const blockById = (id: string) => readBlocksById(store)[id];
 export const isFetching = () => readIsFetching(store);
 
 export default getters;
diff --git a/src/store/blocks/index.ts b/src/store/blocks/index.ts
index dbaf894c2..a4e90507b 100644
--- a/src/store/blocks/index.ts
+++ b/src/store/blocks/index.ts
@@ -3,6 +3,9 @@ import getters from './getters';
 import mutations from './mutations';
 
 const blocks = {
+  getters,
+  actions,
+  mutations,
   namespaced: true,
   strict: true,
   state: {
@@ -10,9 +13,6 @@ const blocks = {
     byId: {},
     fetching: false,
   },
-  getters,
-  actions,
-  mutations,
 };
 
 export default blocks;
diff --git a/src/store/blocks/state.ts b/src/store/blocks/state.ts
index 1743219ec..8ad5e3876 100644
--- a/src/store/blocks/state.ts
+++ b/src/store/blocks/state.ts
@@ -5,7 +5,7 @@ import { OneWireTempSensorBlock, OneWireTempSensor } from './OneWireTempSensor/O
 import { State as RootState } from '../state';
 
 export interface BlockBase {
-  id: string,
+  id: string;
 }
 
 export type Block = SetPointSimpleBlock | OneWireTempSensorBlock;
diff --git a/src/vue-shims.d.ts b/src/vue-shims.d.ts
index 9bb374711..05ee2ac80 100644
--- a/src/vue-shims.d.ts
+++ b/src/vue-shims.d.ts
@@ -1,9 +1,9 @@
 // standard declarations for *.vue files
 declare module '*.vue' {
-  import Vue from 'vue'; // eslint-disable-line
+  import Vue from 'vue';
   export default Vue;
 }
 
 // Quasar specific declarations
 declare module 'quasar';
-declare const __THEME: string; // eslint-disable-line
+declare const __THEME: string;
diff --git a/tsconfig.json b/tsconfig.json
index bb7d9b318..e4f991efe 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -4,6 +4,7 @@
     "strict": true,
     "lib": ["ESNext", "DOM"],
     "moduleResolution": "node",
-    "sourceMap": true
+    "sourceMap": true,
+    "experimentalDecorators": true
   }
 }
diff --git a/tslint.json b/tslint.json
new file mode 100644
index 000000000..740a3e241
--- /dev/null
+++ b/tslint.json
@@ -0,0 +1,12 @@
+{
+    "defaultSeverity": "error",
+    "extends": [
+        "tslint-config-airbnb"
+    ],
+    "jsRules": {},
+    "rules": {
+      "import-name": false,
+      "no-boolean-literal-compare": false
+    },
+    "rulesDirectory": []
+}