From c60d0e7392fc708e961614d6b756a045de74f458 Mon Sep 17 00:00:00 2001
From: Shin'ya Ueoka <ueokande@i-beam.org>
Date: Tue, 30 Apr 2019 14:00:07 +0900
Subject: Rename .js/.jsx to .ts/.tsx

---
 src/content/components/top-content/find.js         |  41 ------
 src/content/components/top-content/find.ts         |  41 ++++++
 .../components/top-content/follow-controller.js    | 147 ---------------------
 .../components/top-content/follow-controller.ts    | 147 +++++++++++++++++++++
 src/content/components/top-content/index.js        |  41 ------
 src/content/components/top-content/index.ts        |  41 ++++++
 6 files changed, 229 insertions(+), 229 deletions(-)
 delete mode 100644 src/content/components/top-content/find.js
 create mode 100644 src/content/components/top-content/find.ts
 delete mode 100644 src/content/components/top-content/follow-controller.js
 create mode 100644 src/content/components/top-content/follow-controller.ts
 delete mode 100644 src/content/components/top-content/index.js
 create mode 100644 src/content/components/top-content/index.ts

(limited to 'src/content/components/top-content')

diff --git a/src/content/components/top-content/find.js b/src/content/components/top-content/find.js
deleted file mode 100644
index 4d46d79..0000000
--- a/src/content/components/top-content/find.js
+++ /dev/null
@@ -1,41 +0,0 @@
-import * as findActions from 'content/actions/find';
-import messages from 'shared/messages';
-
-export default class FindComponent {
-  constructor(win, store) {
-    this.win = win;
-    this.store = store;
-
-    messages.onMessage(this.onMessage.bind(this));
-  }
-
-  onMessage(message) {
-    switch (message.type) {
-    case messages.CONSOLE_ENTER_FIND:
-      return this.start(message.text);
-    case messages.FIND_NEXT:
-      return this.next();
-    case messages.FIND_PREV:
-      return this.prev();
-    }
-  }
-
-  start(text) {
-    let state = this.store.getState().find;
-
-    if (text.length === 0) {
-      return this.store.dispatch(findActions.next(state.keyword, true));
-    }
-    return this.store.dispatch(findActions.next(text, true));
-  }
-
-  next() {
-    let state = this.store.getState().find;
-    return this.store.dispatch(findActions.next(state.keyword, false));
-  }
-
-  prev() {
-    let state = this.store.getState().find;
-    return this.store.dispatch(findActions.prev(state.keyword, false));
-  }
-}
diff --git a/src/content/components/top-content/find.ts b/src/content/components/top-content/find.ts
new file mode 100644
index 0000000..4d46d79
--- /dev/null
+++ b/src/content/components/top-content/find.ts
@@ -0,0 +1,41 @@
+import * as findActions from 'content/actions/find';
+import messages from 'shared/messages';
+
+export default class FindComponent {
+  constructor(win, store) {
+    this.win = win;
+    this.store = store;
+
+    messages.onMessage(this.onMessage.bind(this));
+  }
+
+  onMessage(message) {
+    switch (message.type) {
+    case messages.CONSOLE_ENTER_FIND:
+      return this.start(message.text);
+    case messages.FIND_NEXT:
+      return this.next();
+    case messages.FIND_PREV:
+      return this.prev();
+    }
+  }
+
+  start(text) {
+    let state = this.store.getState().find;
+
+    if (text.length === 0) {
+      return this.store.dispatch(findActions.next(state.keyword, true));
+    }
+    return this.store.dispatch(findActions.next(text, true));
+  }
+
+  next() {
+    let state = this.store.getState().find;
+    return this.store.dispatch(findActions.next(state.keyword, false));
+  }
+
+  prev() {
+    let state = this.store.getState().find;
+    return this.store.dispatch(findActions.prev(state.keyword, false));
+  }
+}
diff --git a/src/content/components/top-content/follow-controller.js b/src/content/components/top-content/follow-controller.js
deleted file mode 100644
index 7f36604..0000000
--- a/src/content/components/top-content/follow-controller.js
+++ /dev/null
@@ -1,147 +0,0 @@
-import * as followControllerActions from 'content/actions/follow-controller';
-import messages from 'shared/messages';
-import HintKeyProducer from 'content/hint-key-producer';
-import * as properties from 'shared/settings/properties';
-
-const broadcastMessage = (win, message) => {
-  let json = JSON.stringify(message);
-  let frames = [window.self].concat(Array.from(window.frames));
-  frames.forEach(frame => frame.postMessage(json, '*'));
-};
-
-export default class FollowController {
-  constructor(win, store) {
-    this.win = win;
-    this.store = store;
-    this.state = {};
-    this.keys = [];
-    this.producer = null;
-
-    messages.onMessage(this.onMessage.bind(this));
-
-    store.subscribe(() => {
-      this.update();
-    });
-  }
-
-  onMessage(message, sender) {
-    switch (message.type) {
-    case messages.FOLLOW_START:
-      return this.store.dispatch(
-        followControllerActions.enable(message.newTab, message.background));
-    case messages.FOLLOW_RESPONSE_COUNT_TARGETS:
-      return this.create(message.count, sender);
-    case messages.FOLLOW_KEY_PRESS:
-      return this.keyPress(message.key, message.ctrlKey);
-    }
-  }
-
-  update() {
-    let prevState = this.state;
-    this.state = this.store.getState().followController;
-
-    if (!prevState.enabled && this.state.enabled) {
-      this.count();
-    } else if (prevState.enabled && !this.state.enabled) {
-      this.remove();
-    } else if (prevState.keys !== this.state.keys) {
-      this.updateHints();
-    }
-  }
-
-  updateHints() {
-    let shown = this.keys.filter(key => key.startsWith(this.state.keys));
-    if (shown.length === 1) {
-      this.activate();
-      this.store.dispatch(followControllerActions.disable());
-    }
-
-    broadcastMessage(this.win, {
-      type: messages.FOLLOW_SHOW_HINTS,
-      keys: this.state.keys,
-    });
-  }
-
-  activate() {
-    broadcastMessage(this.win, {
-      type: messages.FOLLOW_ACTIVATE,
-      keys: this.state.keys,
-    });
-  }
-
-  keyPress(key, ctrlKey) {
-    if (key === '[' && ctrlKey) {
-      this.store.dispatch(followControllerActions.disable());
-      return true;
-    }
-    switch (key) {
-    case 'Enter':
-      this.activate();
-      this.store.dispatch(followControllerActions.disable());
-      break;
-    case 'Esc':
-      this.store.dispatch(followControllerActions.disable());
-      break;
-    case 'Backspace':
-    case 'Delete':
-      this.store.dispatch(followControllerActions.backspace());
-      break;
-    default:
-      if (this.hintchars().includes(key)) {
-        this.store.dispatch(followControllerActions.keyPress(key));
-      }
-      break;
-    }
-    return true;
-  }
-
-  count() {
-    this.producer = new HintKeyProducer(this.hintchars());
-    let doc = this.win.document;
-    let viewWidth = this.win.innerWidth || doc.documentElement.clientWidth;
-    let viewHeight = this.win.innerHeight || doc.documentElement.clientHeight;
-    let frameElements = this.win.document.querySelectorAll('frame,iframe');
-
-    this.win.postMessage(JSON.stringify({
-      type: messages.FOLLOW_REQUEST_COUNT_TARGETS,
-      viewSize: { width: viewWidth, height: viewHeight },
-      framePosition: { x: 0, y: 0 },
-    }), '*');
-    frameElements.forEach((element) => {
-      let { left: frameX, top: frameY } = element.getBoundingClientRect();
-      let message = JSON.stringify({
-        type: messages.FOLLOW_REQUEST_COUNT_TARGETS,
-        viewSize: { width: viewWidth, height: viewHeight },
-        framePosition: { x: frameX, y: frameY },
-      });
-      element.contentWindow.postMessage(message, '*');
-    });
-  }
-
-  create(count, sender) {
-    let produced = [];
-    for (let i = 0; i < count; ++i) {
-      produced.push(this.producer.produce());
-    }
-    this.keys = this.keys.concat(produced);
-
-    sender.postMessage(JSON.stringify({
-      type: messages.FOLLOW_CREATE_HINTS,
-      keysArray: produced,
-      newTab: this.state.newTab,
-      background: this.state.background,
-    }), '*');
-  }
-
-  remove() {
-    this.keys = [];
-    broadcastMessage(this.win, {
-      type: messages.FOLLOW_REMOVE_HINTS,
-    });
-  }
-
-  hintchars() {
-    return this.store.getState().setting.properties.hintchars ||
-      properties.defaults.hintchars;
-  }
-}
diff --git a/src/content/components/top-content/follow-controller.ts b/src/content/components/top-content/follow-controller.ts
new file mode 100644
index 0000000..7f36604
--- /dev/null
+++ b/src/content/components/top-content/follow-controller.ts
@@ -0,0 +1,147 @@
+import * as followControllerActions from 'content/actions/follow-controller';
+import messages from 'shared/messages';
+import HintKeyProducer from 'content/hint-key-producer';
+import * as properties from 'shared/settings/properties';
+
+const broadcastMessage = (win, message) => {
+  let json = JSON.stringify(message);
+  let frames = [window.self].concat(Array.from(window.frames));
+  frames.forEach(frame => frame.postMessage(json, '*'));
+};
+
+export default class FollowController {
+  constructor(win, store) {
+    this.win = win;
+    this.store = store;
+    this.state = {};
+    this.keys = [];
+    this.producer = null;
+
+    messages.onMessage(this.onMessage.bind(this));
+
+    store.subscribe(() => {
+      this.update();
+    });
+  }
+
+  onMessage(message, sender) {
+    switch (message.type) {
+    case messages.FOLLOW_START:
+      return this.store.dispatch(
+        followControllerActions.enable(message.newTab, message.background));
+    case messages.FOLLOW_RESPONSE_COUNT_TARGETS:
+      return this.create(message.count, sender);
+    case messages.FOLLOW_KEY_PRESS:
+      return this.keyPress(message.key, message.ctrlKey);
+    }
+  }
+
+  update() {
+    let prevState = this.state;
+    this.state = this.store.getState().followController;
+
+    if (!prevState.enabled && this.state.enabled) {
+      this.count();
+    } else if (prevState.enabled && !this.state.enabled) {
+      this.remove();
+    } else if (prevState.keys !== this.state.keys) {
+      this.updateHints();
+    }
+  }
+
+  updateHints() {
+    let shown = this.keys.filter(key => key.startsWith(this.state.keys));
+    if (shown.length === 1) {
+      this.activate();
+      this.store.dispatch(followControllerActions.disable());
+    }
+
+    broadcastMessage(this.win, {
+      type: messages.FOLLOW_SHOW_HINTS,
+      keys: this.state.keys,
+    });
+  }
+
+  activate() {
+    broadcastMessage(this.win, {
+      type: messages.FOLLOW_ACTIVATE,
+      keys: this.state.keys,
+    });
+  }
+
+  keyPress(key, ctrlKey) {
+    if (key === '[' && ctrlKey) {
+      this.store.dispatch(followControllerActions.disable());
+      return true;
+    }
+    switch (key) {
+    case 'Enter':
+      this.activate();
+      this.store.dispatch(followControllerActions.disable());
+      break;
+    case 'Esc':
+      this.store.dispatch(followControllerActions.disable());
+      break;
+    case 'Backspace':
+    case 'Delete':
+      this.store.dispatch(followControllerActions.backspace());
+      break;
+    default:
+      if (this.hintchars().includes(key)) {
+        this.store.dispatch(followControllerActions.keyPress(key));
+      }
+      break;
+    }
+    return true;
+  }
+
+  count() {
+    this.producer = new HintKeyProducer(this.hintchars());
+    let doc = this.win.document;
+    let viewWidth = this.win.innerWidth || doc.documentElement.clientWidth;
+    let viewHeight = this.win.innerHeight || doc.documentElement.clientHeight;
+    let frameElements = this.win.document.querySelectorAll('frame,iframe');
+
+    this.win.postMessage(JSON.stringify({
+      type: messages.FOLLOW_REQUEST_COUNT_TARGETS,
+      viewSize: { width: viewWidth, height: viewHeight },
+      framePosition: { x: 0, y: 0 },
+    }), '*');
+    frameElements.forEach((element) => {
+      let { left: frameX, top: frameY } = element.getBoundingClientRect();
+      let message = JSON.stringify({
+        type: messages.FOLLOW_REQUEST_COUNT_TARGETS,
+        viewSize: { width: viewWidth, height: viewHeight },
+        framePosition: { x: frameX, y: frameY },
+      });
+      element.contentWindow.postMessage(message, '*');
+    });
+  }
+
+  create(count, sender) {
+    let produced = [];
+    for (let i = 0; i < count; ++i) {
+      produced.push(this.producer.produce());
+    }
+    this.keys = this.keys.concat(produced);
+
+    sender.postMessage(JSON.stringify({
+      type: messages.FOLLOW_CREATE_HINTS,
+      keysArray: produced,
+      newTab: this.state.newTab,
+      background: this.state.background,
+    }), '*');
+  }
+
+  remove() {
+    this.keys = [];
+    broadcastMessage(this.win, {
+      type: messages.FOLLOW_REMOVE_HINTS,
+    });
+  }
+
+  hintchars() {
+    return this.store.getState().setting.properties.hintchars ||
+      properties.defaults.hintchars;
+  }
+}
diff --git a/src/content/components/top-content/index.js b/src/content/components/top-content/index.js
deleted file mode 100644
index 1aaef1b..0000000
--- a/src/content/components/top-content/index.js
+++ /dev/null
@@ -1,41 +0,0 @@
-import CommonComponent from '../common';
-import FollowController from './follow-controller';
-import FindComponent from './find';
-import * as consoleFrames from '../../console-frames';
-import messages from 'shared/messages';
-import * as scrolls from 'content/scrolls';
-
-export default class TopContent {
-
-  constructor(win, store) {
-    this.win = win;
-    this.store = store;
-
-    new CommonComponent(win, store); // eslint-disable-line no-new
-    new FollowController(win, store); // eslint-disable-line no-new
-    new FindComponent(win, store); // eslint-disable-line no-new
-
-    // TODO make component
-    consoleFrames.initialize(this.win.document);
-
-    messages.onMessage(this.onMessage.bind(this));
-  }
-
-  onMessage(message) {
-    let addonState = this.store.getState().addon;
-
-    switch (message.type) {
-    case messages.CONSOLE_UNFOCUS:
-      this.win.focus();
-      consoleFrames.blur(window.document);
-      return Promise.resolve();
-    case messages.ADDON_ENABLED_QUERY:
-      return Promise.resolve({
-        type: messages.ADDON_ENABLED_RESPONSE,
-        enabled: addonState.enabled,
-      });
-    case messages.TAB_SCROLL_TO:
-      return scrolls.scrollTo(message.x, message.y, false);
-    }
-  }
-}
diff --git a/src/content/components/top-content/index.ts b/src/content/components/top-content/index.ts
new file mode 100644
index 0000000..1aaef1b
--- /dev/null
+++ b/src/content/components/top-content/index.ts
@@ -0,0 +1,41 @@
+import CommonComponent from '../common';
+import FollowController from './follow-controller';
+import FindComponent from './find';
+import * as consoleFrames from '../../console-frames';
+import messages from 'shared/messages';
+import * as scrolls from 'content/scrolls';
+
+export default class TopContent {
+
+  constructor(win, store) {
+    this.win = win;
+    this.store = store;
+
+    new CommonComponent(win, store); // eslint-disable-line no-new
+    new FollowController(win, store); // eslint-disable-line no-new
+    new FindComponent(win, store); // eslint-disable-line no-new
+
+    // TODO make component
+    consoleFrames.initialize(this.win.document);
+
+    messages.onMessage(this.onMessage.bind(this));
+  }
+
+  onMessage(message) {
+    let addonState = this.store.getState().addon;
+
+    switch (message.type) {
+    case messages.CONSOLE_UNFOCUS:
+      this.win.focus();
+      consoleFrames.blur(window.document);
+      return Promise.resolve();
+    case messages.ADDON_ENABLED_QUERY:
+      return Promise.resolve({
+        type: messages.ADDON_ENABLED_RESPONSE,
+        enabled: addonState.enabled,
+      });
+    case messages.TAB_SCROLL_TO:
+      return scrolls.scrollTo(message.x, message.y, false);
+    }
+  }
+}
-- 
cgit v1.2.3


From d01db82c0dca352de2d7644c383d388fc3ec0366 Mon Sep 17 00:00:00 2001
From: Shin'ya Ueoka <ueokande@i-beam.org>
Date: Thu, 2 May 2019 14:08:51 +0900
Subject: Types src/content

---
 .eslintrc                                          |   2 +
 package.json                                       |   1 +
 src/background/controllers/OperationController.ts  |   4 +-
 src/background/controllers/VersionController.ts    |   2 +-
 src/background/domains/Setting.ts                  |  20 +-
 src/background/infrastructures/ConsoleClient.ts    |   2 +-
 .../infrastructures/ContentMessageClient.ts        |   2 +-
 .../infrastructures/ContentMessageListener.ts      |   6 +-
 src/background/presenters/NotifyPresenter.ts       |   6 +-
 src/background/usecases/VersionUseCase.ts          |   2 +-
 src/console/actions/console.ts                     |   2 +-
 src/console/index.tsx                              |  11 +-
 src/content/MessageListener.ts                     |  32 ++
 src/content/actions/addon.ts                       |  10 +-
 src/content/actions/find.ts                        |  40 +-
 src/content/actions/follow-controller.ts           |  12 +-
 src/content/actions/index.ts                       | 151 ++++--
 src/content/actions/input.ts                       |   6 +-
 src/content/actions/mark.ts                        |  20 +-
 src/content/actions/operation.ts                   |  25 +-
 src/content/actions/setting.ts                     |  14 +-
 src/content/components/common/follow.ts            |  79 +++-
 src/content/components/common/hint.ts              |  33 +-
 src/content/components/common/index.ts             |  43 +-
 src/content/components/common/input.ts             |  46 +-
 src/content/components/common/keymapper.ts         |   8 +-
 src/content/components/common/mark.ts              |   2 +-
 src/content/components/top-content/find.ts         |  25 +-
 .../components/top-content/follow-controller.ts    |  65 ++-
 src/content/components/top-content/index.ts        |  28 +-
 src/content/console-frames.ts                      |  18 +-
 src/content/focuses.ts                             |   8 +-
 src/content/hint-key-producer.ts                   |  10 +-
 src/content/index.ts                               |   9 +-
 src/content/navigates.ts                           |  45 +-
 src/content/reducers/addon.ts                      |  13 +-
 src/content/reducers/find.ts                       |  14 +-
 src/content/reducers/follow-controller.ts          |  16 +-
 src/content/reducers/index.ts                      |  22 +-
 src/content/reducers/input.ts                      |  13 +-
 src/content/reducers/mark.ts                       |  20 +-
 src/content/reducers/setting.ts                    |  11 +-
 src/content/scrolls.ts                             |  38 +-
 src/content/store/index.ts                         |   8 +
 src/content/urls.ts                                |   8 +-
 src/shared/messages.ts                             | 346 +++++++++++---
 src/shared/operations.ts                           | 523 ++++++++++++++++++---
 src/shared/settings/validator.ts                   |   2 +-
 src/shared/utils/keys.ts                           |   2 +-
 test/content/actions/follow-controller.test.ts     |   2 +-
 test/content/actions/input.test.ts                 |   2 +-
 test/content/actions/mark.test.ts                  |   2 +-
 test/content/actions/setting.test.ts               |   2 +-
 test/content/components/common/input.test.ts       |  14 +-
 test/content/reducers/addon.test.ts                |   2 +-
 test/content/reducers/find.test.ts                 |   2 +-
 test/content/reducers/follow-controller.test.ts    |   2 +-
 test/content/reducers/input.test.ts                |   2 +-
 test/content/reducers/mark.test.ts                 |   2 +-
 test/content/reducers/setting.test.ts              |   2 +-
 test/shared/operations.test.ts                     |  41 ++
 tsconfig.json                                      |   9 +-
 62 files changed, 1426 insertions(+), 483 deletions(-)
 create mode 100644 src/content/MessageListener.ts
 create mode 100644 src/content/store/index.ts
 create mode 100644 test/shared/operations.test.ts

(limited to 'src/content/components/top-content')

diff --git a/.eslintrc b/.eslintrc
index fb60bc2..7845ca5 100644
--- a/.eslintrc
+++ b/.eslintrc
@@ -35,6 +35,7 @@
     "indent": ["error", 2],
     "jsx-quotes": ["error", "prefer-single"],
     "max-classes-per-file": "off",
+    "max-lines": "off",
     "max-params": ["error", 5],
     "max-statements": ["error", 15],
     "multiline-comment-style": "off",
@@ -47,6 +48,7 @@
     "no-console": ["error", { "allow": ["warn", "error"] }],
     "no-continue": "off",
     "no-empty-function": "off",
+    "no-extra-parens": "off",
     "no-magic-numbers": "off",
     "no-mixed-operators": "off",
     "no-plusplus": "off",
diff --git a/package.json b/package.json
index 5d44a1b..a799554 100644
--- a/package.json
+++ b/package.json
@@ -6,6 +6,7 @@
     "build": "NODE_ENV=production webpack --mode production --progress --display-error-details",
     "package": "npm run build && script/package",
     "lint": "eslint --ext .js,.jsx,.ts,.tsx src",
+    "type-checks": "tsc",
     "test": "karma start",
     "test:e2e": "mocha --timeout 8000 e2e"
   },
diff --git a/src/background/controllers/OperationController.ts b/src/background/controllers/OperationController.ts
index 4e9c106..fa09512 100644
--- a/src/background/controllers/OperationController.ts
+++ b/src/background/controllers/OperationController.ts
@@ -1,4 +1,4 @@
-import operations from '../../shared/operations';
+import * as operations from '../../shared/operations';
 import FindUseCase from '../usecases/FindUseCase';
 import ConsoleUseCase from '../usecases/ConsoleUseCase';
 import TabUseCase from '../usecases/TabUseCase';
@@ -25,7 +25,7 @@ export default class OperationController {
   }
 
   // eslint-disable-next-line complexity, max-lines-per-function
-  exec(operation: any): Promise<any> {
+  exec(operation: operations.Operation): Promise<any> {
     switch (operation.type) {
     case operations.TAB_CLOSE:
       return this.tabUseCase.close(false);
diff --git a/src/background/controllers/VersionController.ts b/src/background/controllers/VersionController.ts
index f402ed0..2e2a197 100644
--- a/src/background/controllers/VersionController.ts
+++ b/src/background/controllers/VersionController.ts
@@ -7,7 +7,7 @@ export default class VersionController {
     this.versionUseCase = new VersionUseCase();
   }
 
-  notify(): void {
+  notify(): Promise<void> {
     return this.versionUseCase.notify();
   }
 }
diff --git a/src/background/domains/Setting.ts b/src/background/domains/Setting.ts
index 106ec0f..b2b1ff2 100644
--- a/src/background/domains/Setting.ts
+++ b/src/background/domains/Setting.ts
@@ -1,22 +1,30 @@
 import DefaultSettings from '../../shared/settings/default';
 import * as settingsValues from '../../shared/settings/values';
 
+type SettingValue = {
+    source: string,
+    json: string,
+    form: any
+}
+
 export default class Setting {
-  constructor({ source, json, form }) {
+  private obj: SettingValue;
+
+  constructor({ source, json, form }: SettingValue) {
     this.obj = {
       source, json, form
     };
   }
 
-  get source() {
+  get source(): string {
     return this.obj.source;
   }
 
-  get json() {
+  get json(): string {
     return this.obj.json;
   }
 
-  get form() {
+  get form(): any {
     return this.obj.form;
   }
 
@@ -33,11 +41,11 @@ export default class Setting {
     return { ...settingsValues.valueFromJson(DefaultSettings.json), ...value };
   }
 
-  serialize() {
+  serialize(): SettingValue {
     return this.obj;
   }
 
-  static deserialize(obj) {
+  static deserialize(obj: SettingValue): Setting {
     return new Setting({ source: obj.source, json: obj.json, form: obj.form });
   }
 
diff --git a/src/background/infrastructures/ConsoleClient.ts b/src/background/infrastructures/ConsoleClient.ts
index 7ad5d24..c162634 100644
--- a/src/background/infrastructures/ConsoleClient.ts
+++ b/src/background/infrastructures/ConsoleClient.ts
@@ -1,4 +1,4 @@
-import messages from '../../shared/messages';
+import * as messages from '../../shared/messages';
 
 export default class ConsoleClient {
   showCommand(tabId: number, command: string): Promise<any> {
diff --git a/src/background/infrastructures/ContentMessageClient.ts b/src/background/infrastructures/ContentMessageClient.ts
index 20057c7..d4bc476 100644
--- a/src/background/infrastructures/ContentMessageClient.ts
+++ b/src/background/infrastructures/ContentMessageClient.ts
@@ -1,4 +1,4 @@
-import messages from '../../shared/messages';
+import * as messages from '../../shared/messages';
 
 export default class ContentMessageClient {
   async broadcastSettingsChanged(): Promise<void> {
diff --git a/src/background/infrastructures/ContentMessageListener.ts b/src/background/infrastructures/ContentMessageListener.ts
index 81d3232..1cc2696 100644
--- a/src/background/infrastructures/ContentMessageListener.ts
+++ b/src/background/infrastructures/ContentMessageListener.ts
@@ -1,4 +1,4 @@
-import messages from '../../shared/messages';
+import * as messages from '../../shared/messages';
 import CompletionGroup from '../domains/CompletionGroup';
 import CommandController from '../controllers/CommandController';
 import SettingController from '../controllers/SettingController';
@@ -68,7 +68,9 @@ export default class ContentMessageListener {
     browser.runtime.onConnect.addListener(this.onConnected.bind(this));
   }
 
-  onMessage(message: any, senderTab: browser.tabs.Tab): Promise<any> | any {
+  onMessage(
+    message: messages.Message, senderTab: browser.tabs.Tab,
+  ): Promise<any> | any {
     switch (message.type) {
     case messages.CONSOLE_QUERY_COMPLETIONS:
       return this.onConsoleQueryCompletions(message.text);
diff --git a/src/background/presenters/NotifyPresenter.ts b/src/background/presenters/NotifyPresenter.ts
index c83c205..23932f7 100644
--- a/src/background/presenters/NotifyPresenter.ts
+++ b/src/background/presenters/NotifyPresenter.ts
@@ -1,11 +1,11 @@
 const NOTIFICATION_ID = 'vimvixen-update';
 
 export default class NotifyPresenter {
-  notify(
+  async notify(
     title: string,
     message: string,
     onclick: () => void,
-  ): Promise<string> {
+  ): Promise<void> {
     const listener = (id: string) => {
       if (id !== NOTIFICATION_ID) {
         return;
@@ -17,7 +17,7 @@ export default class NotifyPresenter {
     };
     browser.notifications.onClicked.addListener(listener);
 
-    return browser.notifications.create(NOTIFICATION_ID, {
+    await browser.notifications.create(NOTIFICATION_ID, {
       'type': 'basic',
       'iconUrl': browser.extension.getURL('resources/icon_48x48.png'),
       title,
diff --git a/src/background/usecases/VersionUseCase.ts b/src/background/usecases/VersionUseCase.ts
index 207f9e2..3a3cc2e 100644
--- a/src/background/usecases/VersionUseCase.ts
+++ b/src/background/usecases/VersionUseCase.ts
@@ -12,7 +12,7 @@ export default class VersionUseCase {
     this.notifyPresenter = new NotifyPresenter();
   }
 
-  notify(): Promise<string> {
+  notify(): Promise<void> {
     let title = `Vim Vixen ${manifest.version} has been installed`;
     let message = 'Click here to see release notes';
     let url = this.releaseNoteUrl(manifest.version);
diff --git a/src/console/actions/console.ts b/src/console/actions/console.ts
index ceb419c..b1494b0 100644
--- a/src/console/actions/console.ts
+++ b/src/console/actions/console.ts
@@ -1,4 +1,4 @@
-import messages from '../../shared/messages';
+import * as messages from '../../shared/messages';
 import * as actions from './index';
 
 const hide = (): actions.ConsoleAction => {
diff --git a/src/console/index.tsx b/src/console/index.tsx
index ee3a8ee..b655154 100644
--- a/src/console/index.tsx
+++ b/src/console/index.tsx
@@ -1,4 +1,4 @@
-import messages from '../shared/messages';
+import * as messages from '../shared/messages';
 import reducers from './reducers';
 import { createStore, applyMiddleware } from 'redux';
 import promise from 'redux-promise';
@@ -23,15 +23,16 @@ window.addEventListener('load', () => {
 });
 
 const onMessage = (message: any): any => {
-  switch (message.type) {
+  let msg = messages.valueOf(message);
+  switch (msg.type) {
   case messages.CONSOLE_SHOW_COMMAND:
-    return store.dispatch(consoleActions.showCommand(message.command));
+    return store.dispatch(consoleActions.showCommand(msg.command));
   case messages.CONSOLE_SHOW_FIND:
     return store.dispatch(consoleActions.showFind());
   case messages.CONSOLE_SHOW_ERROR:
-    return store.dispatch(consoleActions.showError(message.text));
+    return store.dispatch(consoleActions.showError(msg.text));
   case messages.CONSOLE_SHOW_INFO:
-    return store.dispatch(consoleActions.showInfo(message.text));
+    return store.dispatch(consoleActions.showInfo(msg.text));
   case messages.CONSOLE_HIDE:
     return store.dispatch(consoleActions.hide());
   }
diff --git a/src/content/MessageListener.ts b/src/content/MessageListener.ts
new file mode 100644
index 0000000..105d028
--- /dev/null
+++ b/src/content/MessageListener.ts
@@ -0,0 +1,32 @@
+import { Message, valueOf } from '../shared/messages';
+
+export type WebMessageSender = Window | MessagePort | ServiceWorker | null;
+export type WebExtMessageSender = browser.runtime.MessageSender;
+
+export default class MessageListener {
+  onWebMessage(
+    listener: (msg: Message, sender: WebMessageSender) => void,
+  ) {
+    window.addEventListener('message', (event: MessageEvent) => {
+      let sender = event.source;
+      let message = null;
+      try {
+        message = JSON.parse(event.data);
+      } catch (e) {
+        // ignore unexpected message
+        return;
+      }
+      listener(message, sender);
+    });
+  }
+
+  onBackgroundMessage(
+    listener: (msg: Message, sender: WebExtMessageSender) => any,
+  ) {
+    browser.runtime.onMessage.addListener(
+      (msg: any, sender: WebExtMessageSender) => {
+        listener(valueOf(msg), sender);
+      },
+    );
+  }
+}
diff --git a/src/content/actions/addon.ts b/src/content/actions/addon.ts
index b30cf16..8dedae0 100644
--- a/src/content/actions/addon.ts
+++ b/src/content/actions/addon.ts
@@ -1,11 +1,11 @@
-import messages from 'shared/messages';
-import actions from 'content/actions';
+import * as messages from '../../shared/messages';
+import * as actions from './index';
 
-const enable = () => setEnabled(true);
+const enable = (): Promise<actions.AddonAction> => setEnabled(true);
 
-const disable = () => setEnabled(false);
+const disable = (): Promise<actions.AddonAction> => setEnabled(false);
 
-const setEnabled = async(enabled) => {
+const setEnabled = async(enabled: boolean): Promise<actions.AddonAction> => {
   await browser.runtime.sendMessage({
     type: messages.ADDON_ENABLED_RESPONSE,
     enabled,
diff --git a/src/content/actions/find.ts b/src/content/actions/find.ts
index e08d7e5..6dd2ae6 100644
--- a/src/content/actions/find.ts
+++ b/src/content/actions/find.ts
@@ -5,28 +5,41 @@
 // NOTE: window.find is not standard API
 // https://developer.mozilla.org/en-US/docs/Web/API/Window/find
 
-import messages from 'shared/messages';
-import actions from 'content/actions';
+import * as messages from '../../shared/messages';
+import * as actions from './index';
 import * as consoleFrames from '../console-frames';
 
-const find = (string, backwards) => {
+const find = (str: string, backwards: boolean): boolean => {
   let caseSensitive = false;
   let wrapScan = true;
 
 
   // NOTE: aWholeWord dows not implemented, and aSearchInFrames does not work
   // because of same origin policy
-  let found = window.find(string, caseSensitive, backwards, wrapScan);
+
+  // eslint-disable-next-line no-extra-parens
+  let found = (<any>window).find(str, caseSensitive, backwards, wrapScan);
   if (found) {
     return found;
   }
-  window.getSelection().removeAllRanges();
-  return window.find(string, caseSensitive, backwards, wrapScan);
+  let sel = window.getSelection();
+  if (sel) {
+    sel.removeAllRanges();
+  }
+
+  // eslint-disable-next-line no-extra-parens
+  return (<any>window).find(str, caseSensitive, backwards, wrapScan);
 };
 
-const findNext = async(currentKeyword, reset, backwards) => {
+// eslint-disable-next-line max-statements
+const findNext = async(
+  currentKeyword: string, reset: boolean, backwards: boolean,
+): Promise<actions.FindAction> => {
   if (reset) {
-    window.getSelection().removeAllRanges();
+    let sel = window.getSelection();
+    if (sel) {
+      sel.removeAllRanges();
+    }
   }
 
   let keyword = currentKeyword;
@@ -41,7 +54,8 @@ const findNext = async(currentKeyword, reset, backwards) => {
     });
   }
   if (!keyword) {
-    return consoleFrames.postError('No previous search keywords');
+    await consoleFrames.postError('No previous search keywords');
+    return { type: actions.NOOP };
   }
   let found = find(keyword, backwards);
   if (found) {
@@ -57,11 +71,15 @@ const findNext = async(currentKeyword, reset, backwards) => {
   };
 };
 
-const next = (currentKeyword, reset) => {
+const next = (
+  currentKeyword: string, reset: boolean,
+): Promise<actions.FindAction> => {
   return findNext(currentKeyword, reset, false);
 };
 
-const prev = (currentKeyword, reset) => {
+const prev = (
+  currentKeyword: string, reset: boolean,
+): Promise<actions.FindAction> => {
   return findNext(currentKeyword, reset, true);
 };
 
diff --git a/src/content/actions/follow-controller.ts b/src/content/actions/follow-controller.ts
index 006b248..115b3b6 100644
--- a/src/content/actions/follow-controller.ts
+++ b/src/content/actions/follow-controller.ts
@@ -1,6 +1,8 @@
-import actions from 'content/actions';
+import * as actions from './index';
 
-const enable = (newTab, background) => {
+const enable = (
+  newTab: boolean, background: boolean,
+): actions.FollowAction => {
   return {
     type: actions.FOLLOW_CONTROLLER_ENABLE,
     newTab,
@@ -8,20 +10,20 @@ const enable = (newTab, background) => {
   };
 };
 
-const disable = () => {
+const disable = (): actions.FollowAction => {
   return {
     type: actions.FOLLOW_CONTROLLER_DISABLE,
   };
 };
 
-const keyPress = (key) => {
+const keyPress = (key: string): actions.FollowAction => {
   return {
     type: actions.FOLLOW_CONTROLLER_KEY_PRESS,
     key: key
   };
 };
 
-const backspace = () => {
+const backspace = (): actions.FollowAction => {
   return {
     type: actions.FOLLOW_CONTROLLER_BACKSPACE,
   };
diff --git a/src/content/actions/index.ts b/src/content/actions/index.ts
index 0a16fdf..18d0a69 100644
--- a/src/content/actions/index.ts
+++ b/src/content/actions/index.ts
@@ -1,31 +1,120 @@
-export default {
-  // Enable/disable
-  ADDON_SET_ENABLED: 'addon.set.enabled',
-
-  // Settings
-  SETTING_SET: 'setting.set',
-
-  // User input
-  INPUT_KEY_PRESS: 'input.key.press',
-  INPUT_CLEAR_KEYS: 'input.clear.keys',
-
-  // Completion
-  COMPLETION_SET_ITEMS: 'completion.set.items',
-  COMPLETION_SELECT_NEXT: 'completions.select.next',
-  COMPLETION_SELECT_PREV: 'completions.select.prev',
-
-  // Follow
-  FOLLOW_CONTROLLER_ENABLE: 'follow.controller.enable',
-  FOLLOW_CONTROLLER_DISABLE: 'follow.controller.disable',
-  FOLLOW_CONTROLLER_KEY_PRESS: 'follow.controller.key.press',
-  FOLLOW_CONTROLLER_BACKSPACE: 'follow.controller.backspace',
-
-  // Find
-  FIND_SET_KEYWORD: 'find.set.keyword',
-
-  // Mark
-  MARK_START_SET: 'mark.start.set',
-  MARK_START_JUMP: 'mark.start.jump',
-  MARK_CANCEL: 'mark.cancel',
-  MARK_SET_LOCAL: 'mark.set.local',
-};
+import Redux from 'redux';
+
+// Enable/disable
+export const ADDON_SET_ENABLED = 'addon.set.enabled';
+
+// Find
+export const FIND_SET_KEYWORD = 'find.set.keyword';
+
+// Settings
+export const SETTING_SET = 'setting.set';
+
+// User input
+export const INPUT_KEY_PRESS = 'input.key.press';
+export const INPUT_CLEAR_KEYS = 'input.clear.keys';
+
+// Completion
+export const COMPLETION_SET_ITEMS = 'completion.set.items';
+export const COMPLETION_SELECT_NEXT = 'completions.select.next';
+export const COMPLETION_SELECT_PREV = 'completions.select.prev';
+
+// Follow
+export const FOLLOW_CONTROLLER_ENABLE = 'follow.controller.enable';
+export const FOLLOW_CONTROLLER_DISABLE = 'follow.controller.disable';
+export const FOLLOW_CONTROLLER_KEY_PRESS = 'follow.controller.key.press';
+export const FOLLOW_CONTROLLER_BACKSPACE = 'follow.controller.backspace';
+
+// Mark
+export const MARK_START_SET = 'mark.start.set';
+export const MARK_START_JUMP = 'mark.start.jump';
+export const MARK_CANCEL = 'mark.cancel';
+export const MARK_SET_LOCAL = 'mark.set.local';
+
+export const NOOP = 'noop';
+
+export interface AddonSetEnabledAction extends Redux.Action {
+  type: typeof ADDON_SET_ENABLED;
+  enabled: boolean;
+}
+
+export interface FindSetKeywordAction extends Redux.Action {
+  type: typeof FIND_SET_KEYWORD;
+  keyword: string;
+  found: boolean;
+}
+
+export interface SettingSetAction extends Redux.Action {
+  type: typeof SETTING_SET;
+  value: any;
+}
+
+export interface InputKeyPressAction extends Redux.Action {
+  type: typeof INPUT_KEY_PRESS;
+  key: string;
+}
+
+export interface InputClearKeysAction extends Redux.Action {
+  type: typeof INPUT_CLEAR_KEYS;
+}
+
+export interface FollowControllerEnableAction extends Redux.Action {
+  type: typeof FOLLOW_CONTROLLER_ENABLE;
+  newTab: boolean;
+  background: boolean;
+}
+
+export interface FollowControllerDisableAction extends Redux.Action {
+  type: typeof FOLLOW_CONTROLLER_DISABLE;
+}
+
+export interface FollowControllerKeyPressAction extends Redux.Action {
+  type: typeof FOLLOW_CONTROLLER_KEY_PRESS;
+  key: string;
+}
+
+export interface FollowControllerBackspaceAction extends Redux.Action {
+  type: typeof FOLLOW_CONTROLLER_BACKSPACE;
+}
+
+export interface MarkStartSetAction extends Redux.Action {
+  type: typeof MARK_START_SET;
+}
+
+export interface MarkStartJumpAction extends Redux.Action {
+  type: typeof MARK_START_JUMP;
+}
+
+export interface MarkCancelAction extends Redux.Action {
+  type: typeof MARK_CANCEL;
+}
+
+export interface MarkSetLocalAction extends Redux.Action {
+  type: typeof MARK_SET_LOCAL;
+  key: string;
+  x: number;
+  y: number;
+}
+
+export interface NoopAction extends Redux.Action {
+  type: typeof NOOP;
+}
+
+export type AddonAction = AddonSetEnabledAction;
+export type FindAction = FindSetKeywordAction | NoopAction;
+export type SettingAction = SettingSetAction;
+export type InputAction = InputKeyPressAction | InputClearKeysAction;
+export type FollowAction =
+  FollowControllerEnableAction | FollowControllerDisableAction |
+  FollowControllerKeyPressAction | FollowControllerBackspaceAction;
+export type MarkAction =
+  MarkStartSetAction | MarkStartJumpAction |
+  MarkCancelAction | MarkSetLocalAction | NoopAction;
+
+export type Action =
+  AddonAction |
+  FindAction |
+  SettingAction |
+  InputAction |
+  FollowAction |
+  MarkAction |
+  NoopAction;
diff --git a/src/content/actions/input.ts b/src/content/actions/input.ts
index 465a486..21c912e 100644
--- a/src/content/actions/input.ts
+++ b/src/content/actions/input.ts
@@ -1,13 +1,13 @@
-import actions from 'content/actions';
+import * as actions from './index';
 
-const keyPress = (key) => {
+const keyPress = (key: string): actions.InputAction => {
   return {
     type: actions.INPUT_KEY_PRESS,
     key,
   };
 };
 
-const clearKeys = () => {
+const clearKeys = (): actions.InputAction => {
   return {
     type: actions.INPUT_CLEAR_KEYS
   };
diff --git a/src/content/actions/mark.ts b/src/content/actions/mark.ts
index 712a811..5eb9554 100644
--- a/src/content/actions/mark.ts
+++ b/src/content/actions/mark.ts
@@ -1,19 +1,19 @@
-import actions from 'content/actions';
-import messages from 'shared/messages';
+import * as actions from './index';
+import * as messages from '../../shared/messages';
 
-const startSet = () => {
+const startSet = (): actions.MarkAction => {
   return { type: actions.MARK_START_SET };
 };
 
-const startJump = () => {
+const startJump = (): actions.MarkAction => {
   return { type: actions.MARK_START_JUMP };
 };
 
-const cancel = () => {
+const cancel = (): actions.MarkAction => {
   return { type: actions.MARK_CANCEL };
 };
 
-const setLocal = (key, x, y) => {
+const setLocal = (key: string, x: number, y: number): actions.MarkAction => {
   return {
     type: actions.MARK_SET_LOCAL,
     key,
@@ -22,22 +22,22 @@ const setLocal = (key, x, y) => {
   };
 };
 
-const setGlobal = (key, x, y) => {
+const setGlobal = (key: string, x: number, y: number): actions.MarkAction => {
   browser.runtime.sendMessage({
     type: messages.MARK_SET_GLOBAL,
     key,
     x,
     y,
   });
-  return { type: '' };
+  return { type: actions.NOOP };
 };
 
-const jumpGlobal = (key) => {
+const jumpGlobal = (key: string): actions.MarkAction => {
   browser.runtime.sendMessage({
     type: messages.MARK_JUMP_GLOBAL,
     key,
   });
-  return { type: '' };
+  return { type: actions.NOOP };
 };
 
 export {
diff --git a/src/content/actions/operation.ts b/src/content/actions/operation.ts
index ed9b2cf..6acb407 100644
--- a/src/content/actions/operation.ts
+++ b/src/content/actions/operation.ts
@@ -1,16 +1,21 @@
-import operations from 'shared/operations';
-import messages from 'shared/messages';
-import * as scrolls from 'content/scrolls';
-import * as navigates from 'content/navigates';
-import * as focuses from 'content/focuses';
-import * as urls from 'content/urls';
-import * as consoleFrames from 'content/console-frames';
+import * as operations from '../../shared/operations';
+import * as actions from './index';
+import * as messages from '../../shared/messages';
+import * as scrolls from '../scrolls';
+import * as navigates from '../navigates';
+import * as focuses from '../focuses';
+import * as urls from '../urls';
+import * as consoleFrames from '../console-frames';
 import * as addonActions from './addon';
 import * as markActions from './mark';
-import * as properties from 'shared/settings/properties';
+import * as properties from '../../shared/settings/properties';
 
 // eslint-disable-next-line complexity, max-lines-per-function
-const exec = (operation, settings, addonEnabled) => {
+const exec = (
+  operation: operations.Operation,
+  settings: any,
+  addonEnabled: boolean,
+): Promise<actions.Action> | actions.Action => {
   let smoothscroll = settings.properties.smoothscroll ||
     properties.defaults.smoothscroll;
   switch (operation.type) {
@@ -98,7 +103,7 @@ const exec = (operation, settings, addonEnabled) => {
       operation,
     });
   }
-  return { type: '' };
+  return { type: actions.NOOP };
 };
 
 export { exec };
diff --git a/src/content/actions/setting.ts b/src/content/actions/setting.ts
index 1c15dd7..a8f049a 100644
--- a/src/content/actions/setting.ts
+++ b/src/content/actions/setting.ts
@@ -1,15 +1,15 @@
-import actions from 'content/actions';
-import * as keyUtils from 'shared/utils/keys';
-import operations from 'shared/operations';
-import messages from 'shared/messages';
+import * as actions from './index';
+import * as keyUtils from '../../shared/utils/keys';
+import * as operations from '../../shared/operations';
+import * as messages from '../../shared/messages';
 
 const reservedKeymaps = {
   '<Esc>': { type: operations.CANCEL },
   '<C-[>': { type: operations.CANCEL },
 };
 
-const set = (value) => {
-  let entries = [];
+const set = (value: any): actions.SettingAction => {
+  let entries: any[] = [];
   if (value.keymaps) {
     let keymaps = { ...value.keymaps, ...reservedKeymaps };
     entries = Object.entries(keymaps).map((entry) => {
@@ -27,7 +27,7 @@ const set = (value) => {
   };
 };
 
-const load = async() => {
+const load = async(): Promise<actions.SettingAction> => {
   let settings = await browser.runtime.sendMessage({
     type: messages.SETTINGS_QUERY,
   });
diff --git a/src/content/components/common/follow.ts b/src/content/components/common/follow.ts
index 63ce603..67f2dd9 100644
--- a/src/content/components/common/follow.ts
+++ b/src/content/components/common/follow.ts
@@ -1,6 +1,8 @@
-import messages from 'shared/messages';
+import MessageListener from '../../MessageListener';
 import Hint from './hint';
-import * as dom from 'shared/utils/dom';
+import * as dom from '../../../shared/utils/dom';
+import * as messages from '../../../shared/messages';
+import * as keyUtils from '../../../shared/utils/keys';
 
 const TARGET_SELECTOR = [
   'a', 'button', 'input', 'textarea', 'area',
@@ -8,8 +10,22 @@ const TARGET_SELECTOR = [
   '[role="button"]', 'summary'
 ].join(',');
 
+interface Size {
+  width: number;
+  height: number;
+}
+
+interface Point {
+  x: number;
+  y: number;
+}
 
-const inViewport = (win, element, viewSize, framePosition) => {
+const inViewport = (
+  win: Window,
+  element: Element,
+  viewSize: Size,
+  framePosition: Point,
+): boolean => {
   let {
     top, left, bottom, right
   } = dom.viewportRect(element);
@@ -30,34 +46,44 @@ const inViewport = (win, element, viewSize, framePosition) => {
   return true;
 };
 
-const isAriaHiddenOrAriaDisabled = (win, element) => {
+const isAriaHiddenOrAriaDisabled = (win: Window, element: Element): boolean => {
   if (!element || win.document.documentElement === element) {
     return false;
   }
   for (let attr of ['aria-hidden', 'aria-disabled']) {
-    if (element.hasAttribute(attr)) {
-      let hidden = element.getAttribute(attr).toLowerCase();
+    let value = element.getAttribute(attr);
+    if (value !== null) {
+      let hidden = value.toLowerCase();
       if (hidden === '' || hidden === 'true') {
         return true;
       }
     }
   }
-  return isAriaHiddenOrAriaDisabled(win, element.parentNode);
+  return isAriaHiddenOrAriaDisabled(win, element.parentElement as Element);
 };
 
 export default class Follow {
-  constructor(win, store) {
+  private win: Window;
+
+  private newTab: boolean;
+
+  private background: boolean;
+
+  private hints: {[key: string]: Hint };
+
+  private targets: HTMLElement[] = [];
+
+  constructor(win: Window) {
     this.win = win;
-    this.store = store;
     this.newTab = false;
     this.background = false;
     this.hints = {};
     this.targets = [];
 
-    messages.onMessage(this.onMessage.bind(this));
+    new MessageListener().onWebMessage(this.onMessage.bind(this));
   }
 
-  key(key) {
+  key(key: keyUtils.Key): boolean {
     if (Object.keys(this.hints).length === 0) {
       return false;
     }
@@ -69,7 +95,7 @@ export default class Follow {
     return true;
   }
 
-  openLink(element) {
+  openLink(element: HTMLAreaElement|HTMLAnchorElement) {
     // Browser prevent new tab by link with target='_blank'
     if (!this.newTab && element.getAttribute('target') !== '_blank') {
       element.click();
@@ -90,7 +116,7 @@ export default class Follow {
     });
   }
 
-  countHints(sender, viewSize, framePosition) {
+  countHints(sender: any, viewSize: Size, framePosition: Point) {
     this.targets = Follow.getTargetElements(this.win, viewSize, framePosition);
     sender.postMessage(JSON.stringify({
       type: messages.FOLLOW_RESPONSE_COUNT_TARGETS,
@@ -98,7 +124,7 @@ export default class Follow {
     }), '*');
   }
 
-  createHints(keysArray, newTab, background) {
+  createHints(keysArray: string[], newTab: boolean, background: boolean) {
     if (keysArray.length !== this.targets.length) {
       throw new Error('illegal hint count');
     }
@@ -113,7 +139,7 @@ export default class Follow {
     }
   }
 
-  showHints(keys) {
+  showHints(keys: string) {
     Object.keys(this.hints).filter(key => key.startsWith(keys))
       .forEach(key => this.hints[key].show());
     Object.keys(this.hints).filter(key => !key.startsWith(keys))
@@ -128,18 +154,19 @@ export default class Follow {
     this.targets = [];
   }
 
-  activateHints(keys) {
+  activateHints(keys: string) {
     let hint = this.hints[keys];
     if (!hint) {
       return;
     }
-    let element = hint.target;
+    let element = hint.getTarget();
     switch (element.tagName.toLowerCase()) {
     case 'a':
+      return this.openLink(element as HTMLAnchorElement);
     case 'area':
-      return this.openLink(element);
+      return this.openLink(element as HTMLAreaElement);
     case 'input':
-      switch (element.type) {
+      switch ((element as HTMLInputElement).type) {
       case 'file':
       case 'checkbox':
       case 'radio':
@@ -166,7 +193,7 @@ export default class Follow {
     }
   }
 
-  onMessage(message, sender) {
+  onMessage(message: messages.Message, sender: any) {
     switch (message.type) {
     case messages.FOLLOW_REQUEST_COUNT_TARGETS:
       return this.countHints(sender, message.viewSize, message.framePosition);
@@ -178,19 +205,23 @@ export default class Follow {
     case messages.FOLLOW_ACTIVATE:
       return this.activateHints(message.keys);
     case messages.FOLLOW_REMOVE_HINTS:
-      return this.removeHints(message.keys);
+      return this.removeHints();
     }
   }
 
-  static getTargetElements(win, viewSize, framePosition) {
+  static getTargetElements(
+    win: Window,
+    viewSize:
+    Size, framePosition: Point,
+  ): HTMLElement[] {
     let all = win.document.querySelectorAll(TARGET_SELECTOR);
-    let filtered = Array.prototype.filter.call(all, (element) => {
+    let filtered = Array.prototype.filter.call(all, (element: HTMLElement) => {
       let style = win.getComputedStyle(element);
 
       // AREA's 'display' in Browser style is 'none'
       return (element.tagName === 'AREA' || style.display !== 'none') &&
         style.visibility !== 'hidden' &&
-        element.type !== 'hidden' &&
+        (element as HTMLInputElement).type !== 'hidden' &&
         element.offsetHeight > 0 &&
         !isAriaHiddenOrAriaDisabled(win, element) &&
         inViewport(win, element, viewSize, framePosition);
diff --git a/src/content/components/common/hint.ts b/src/content/components/common/hint.ts
index 1472587..2fcbb0f 100644
--- a/src/content/components/common/hint.ts
+++ b/src/content/components/common/hint.ts
@@ -1,6 +1,11 @@
-import * as dom from 'shared/utils/dom';
+import * as dom from '../../../shared/utils/dom';
 
-const hintPosition = (element) => {
+interface Point {
+  x: number;
+  y: number;
+}
+
+const hintPosition = (element: Element): Point => {
   let { left, top, right, bottom } = dom.viewportRect(element);
 
   if (element.tagName !== 'AREA') {
@@ -14,17 +19,21 @@ const hintPosition = (element) => {
 };
 
 export default class Hint {
-  constructor(target, tag) {
-    if (!(document.body instanceof HTMLElement)) {
-      throw new TypeError('target is not an HTMLElement');
-    }
+  private target: HTMLElement;
 
-    this.target = target;
+  private element: HTMLElement;
 
+  constructor(target: HTMLElement, tag: string) {
     let doc = target.ownerDocument;
+    if (doc === null) {
+      throw new TypeError('ownerDocument is null');
+    }
+
     let { x, y } = hintPosition(target);
     let { scrollX, scrollY } = window;
 
+    this.target = target;
+
     this.element = doc.createElement('span');
     this.element.className = 'vimvixen-hint';
     this.element.textContent = tag;
@@ -35,15 +44,19 @@ export default class Hint {
     doc.body.append(this.element);
   }
 
-  show() {
+  show(): void {
     this.element.style.display = 'inline';
   }
 
-  hide() {
+  hide(): void {
     this.element.style.display = 'none';
   }
 
-  remove() {
+  remove(): void {
     this.element.remove();
   }
+
+  getTarget(): HTMLElement {
+    return this.target;
+  }
 }
diff --git a/src/content/components/common/index.ts b/src/content/components/common/index.ts
index bcab4fa..9b5164e 100644
--- a/src/content/components/common/index.ts
+++ b/src/content/components/common/index.ts
@@ -2,33 +2,37 @@ import InputComponent from './input';
 import FollowComponent from './follow';
 import MarkComponent from './mark';
 import KeymapperComponent from './keymapper';
-import * as settingActions from 'content/actions/setting';
-import messages from 'shared/messages';
+import * as settingActions from '../../actions/setting';
+import * as messages from '../../../shared/messages';
+import MessageListener from '../../MessageListener';
 import * as addonActions from '../../actions/addon';
-import * as blacklists from 'shared/blacklists';
+import * as blacklists from '../../../shared/blacklists';
+import * as keys from '../../../shared/utils/keys';
 
 export default class Common {
-  constructor(win, store) {
-    const input = new InputComponent(win.document.body, store);
-    const follow = new FollowComponent(win, store);
+  private win: Window;
+
+  private store: any;
+
+  constructor(win: Window, store: any) {
+    const input = new InputComponent(win.document.body);
+    const follow = new FollowComponent(win);
     const mark = new MarkComponent(win.document.body, store);
     const keymapper = new KeymapperComponent(store);
 
-    input.onKey(key => follow.key(key));
-    input.onKey(key => mark.key(key));
-    input.onKey(key => keymapper.key(key));
+    input.onKey((key: keys.Key) => follow.key(key));
+    input.onKey((key: keys.Key) => mark.key(key));
+    input.onKey((key: keys.Key) => keymapper.key(key));
 
     this.win = win;
     this.store = store;
-    this.prevEnabled = undefined;
-    this.prevBlacklist = undefined;
 
     this.reloadSettings();
 
-    messages.onMessage(this.onMessage.bind(this));
+    new MessageListener().onBackgroundMessage(this.onMessage.bind(this));
   }
 
-  onMessage(message) {
+  onMessage(message: messages.Message) {
     let { enabled } = this.store.getState().addon;
     switch (message.type) {
     case messages.SETTINGS_CHANGED:
@@ -40,12 +44,13 @@ export default class Common {
 
   reloadSettings() {
     try {
-      this.store.dispatch(settingActions.load()).then(({ value: settings }) => {
-        let enabled = !blacklists.includes(
-          settings.blacklist, this.win.location.href
-        );
-        this.store.dispatch(addonActions.setEnabled(enabled));
-      });
+      this.store.dispatch(settingActions.load())
+        .then(({ value: settings }: any) => {
+          let enabled = !blacklists.includes(
+            settings.blacklist, this.win.location.href
+          );
+          this.store.dispatch(addonActions.setEnabled(enabled));
+        });
     } catch (e) {
       // Sometime sendMessage fails when background script is not ready.
       console.warn(e);
diff --git a/src/content/components/common/input.ts b/src/content/components/common/input.ts
index eefaf10..64eb5f3 100644
--- a/src/content/components/common/input.ts
+++ b/src/content/components/common/input.ts
@@ -1,12 +1,16 @@
-import * as dom from 'shared/utils/dom';
-import * as keys from 'shared/utils/keys';
+import * as dom from '../../../shared/utils/dom';
+import * as keys from '../../../shared/utils/keys';
 
-const cancelKey = (e) => {
+const cancelKey = (e: KeyboardEvent): boolean => {
   return e.key === 'Escape' || e.key === '[' && e.ctrlKey;
 };
 
 export default class InputComponent {
-  constructor(target) {
+  private pressed: {[key: string]: string} = {};
+
+  private onKeyListeners: ((key: keys.Key) => boolean)[] = [];
+
+  constructor(target: HTMLElement) {
     this.pressed = {};
     this.onKeyListeners = [];
 
@@ -15,11 +19,11 @@ export default class InputComponent {
     target.addEventListener('keyup', this.onKeyUp.bind(this));
   }
 
-  onKey(cb) {
+  onKey(cb: (key: keys.Key) => boolean) {
     this.onKeyListeners.push(cb);
   }
 
-  onKeyPress(e) {
+  onKeyPress(e: KeyboardEvent) {
     if (this.pressed[e.key] && this.pressed[e.key] !== 'keypress') {
       return;
     }
@@ -27,7 +31,7 @@ export default class InputComponent {
     this.capture(e);
   }
 
-  onKeyDown(e) {
+  onKeyDown(e: KeyboardEvent) {
     if (this.pressed[e.key] && this.pressed[e.key] !== 'keydown') {
       return;
     }
@@ -35,14 +39,19 @@ export default class InputComponent {
     this.capture(e);
   }
 
-  onKeyUp(e) {
+  onKeyUp(e: KeyboardEvent) {
     delete this.pressed[e.key];
   }
 
-  capture(e) {
-    if (this.fromInput(e)) {
-      if (cancelKey(e) && e.target.blur) {
-        e.target.blur();
+  // eslint-disable-next-line max-statements
+  capture(e: KeyboardEvent) {
+    let target = e.target;
+    if (!(target instanceof HTMLElement)) {
+      return;
+    }
+    if (this.fromInput(target)) {
+      if (cancelKey(e) && target.blur) {
+        target.blur();
       }
       return;
     }
@@ -63,13 +72,10 @@ export default class InputComponent {
     }
   }
 
-  fromInput(e) {
-    if (!e.target) {
-      return false;
-    }
-    return e.target instanceof HTMLInputElement ||
-      e.target instanceof HTMLTextAreaElement ||
-      e.target instanceof HTMLSelectElement ||
-      dom.isContentEditable(e.target);
+  fromInput(e: Element) {
+    return e instanceof HTMLInputElement ||
+      e instanceof HTMLTextAreaElement ||
+      e instanceof HTMLSelectElement ||
+      dom.isContentEditable(e);
   }
 }
diff --git a/src/content/components/common/keymapper.ts b/src/content/components/common/keymapper.ts
index ec0d093..d9c9834 100644
--- a/src/content/components/common/keymapper.ts
+++ b/src/content/components/common/keymapper.ts
@@ -1,7 +1,7 @@
-import * as inputActions from 'content/actions/input';
-import * as operationActions from 'content/actions/operation';
-import operations from 'shared/operations';
-import * as keyUtils from 'shared/utils/keys';
+import * as inputActions from '../../actions/input';
+import * as operationActions from '../../actions/operation';
+import * as operations from '../../../shared/operations';
+import * as keyUtils from '../../../shared/utils/keys';
 
 const mapStartsWith = (mapping, keys) => {
   if (mapping.length < keys.length) {
diff --git a/src/content/components/common/mark.ts b/src/content/components/common/mark.ts
index 0f838a9..500d03b 100644
--- a/src/content/components/common/mark.ts
+++ b/src/content/components/common/mark.ts
@@ -3,7 +3,7 @@ import * as scrolls from 'content/scrolls';
 import * as consoleFrames from 'content/console-frames';
 import * as properties from 'shared/settings/properties';
 
-const cancelKey = (key) => {
+const cancelKey = (key): boolean => {
   return key.key === 'Esc' || key.key === '[' && key.ctrlKey;
 };
 
diff --git a/src/content/components/top-content/find.ts b/src/content/components/top-content/find.ts
index 4d46d79..74b95bc 100644
--- a/src/content/components/top-content/find.ts
+++ b/src/content/components/top-content/find.ts
@@ -1,15 +1,17 @@
-import * as findActions from 'content/actions/find';
-import messages from 'shared/messages';
+import * as findActions from '../../actions/find';
+import * as messages from '../../../shared/messages';
+import MessageListener from '../../MessageListener';
 
 export default class FindComponent {
-  constructor(win, store) {
-    this.win = win;
+  private store: any;
+
+  constructor(store: any) {
     this.store = store;
 
-    messages.onMessage(this.onMessage.bind(this));
+    new MessageListener().onWebMessage(this.onMessage.bind(this));
   }
 
-  onMessage(message) {
+  onMessage(message: messages.Message) {
     switch (message.type) {
     case messages.CONSOLE_ENTER_FIND:
       return this.start(message.text);
@@ -20,22 +22,25 @@ export default class FindComponent {
     }
   }
 
-  start(text) {
+  start(text: string) {
     let state = this.store.getState().find;
 
     if (text.length === 0) {
-      return this.store.dispatch(findActions.next(state.keyword, true));
+      return this.store.dispatch(
+        findActions.next(state.keyword as string, true));
     }
     return this.store.dispatch(findActions.next(text, true));
   }
 
   next() {
     let state = this.store.getState().find;
-    return this.store.dispatch(findActions.next(state.keyword, false));
+    return this.store.dispatch(
+      findActions.next(state.keyword as string, false));
   }
 
   prev() {
     let state = this.store.getState().find;
-    return this.store.dispatch(findActions.prev(state.keyword, false));
+    return this.store.dispatch(
+      findActions.prev(state.keyword as string, false));
   }
 }
diff --git a/src/content/components/top-content/follow-controller.ts b/src/content/components/top-content/follow-controller.ts
index 7f36604..be71f6e 100644
--- a/src/content/components/top-content/follow-controller.ts
+++ b/src/content/components/top-content/follow-controller.ts
@@ -1,30 +1,46 @@
-import * as followControllerActions from 'content/actions/follow-controller';
-import messages from 'shared/messages';
-import HintKeyProducer from 'content/hint-key-producer';
-import * as properties from 'shared/settings/properties';
+import * as followControllerActions from '../../actions/follow-controller';
+import * as messages from '../../../shared/messages';
+import MessageListener, { WebMessageSender } from '../../MessageListener';
+import HintKeyProducer from '../../hint-key-producer';
+import * as properties from '../../../shared/settings/properties';
 
-const broadcastMessage = (win, message) => {
+const broadcastMessage = (win: Window, message: messages.Message): void => {
   let json = JSON.stringify(message);
-  let frames = [window.self].concat(Array.from(window.frames));
+  let frames = [win.self].concat(Array.from(win.frames as any));
   frames.forEach(frame => frame.postMessage(json, '*'));
 };
 
 export default class FollowController {
-  constructor(win, store) {
+  private win: Window;
+
+  private store: any;
+
+  private state: {
+    enabled?: boolean;
+    newTab?: boolean;
+    background?: boolean;
+    keys?: string,
+  };
+
+  private keys: string[];
+
+  private producer: HintKeyProducer | null;
+
+  constructor(win: Window, store: any) {
     this.win = win;
     this.store = store;
     this.state = {};
     this.keys = [];
     this.producer = null;
 
-    messages.onMessage(this.onMessage.bind(this));
+    new MessageListener().onWebMessage(this.onMessage.bind(this));
 
     store.subscribe(() => {
       this.update();
     });
   }
 
-  onMessage(message, sender) {
+  onMessage(message: messages.Message, sender: WebMessageSender) {
     switch (message.type) {
     case messages.FOLLOW_START:
       return this.store.dispatch(
@@ -36,7 +52,7 @@ export default class FollowController {
     }
   }
 
-  update() {
+  update(): void {
     let prevState = this.state;
     this.state = this.store.getState().followController;
 
@@ -49,8 +65,10 @@ export default class FollowController {
     }
   }
 
-  updateHints() {
-    let shown = this.keys.filter(key => key.startsWith(this.state.keys));
+  updateHints(): void {
+    let shown = this.keys.filter((key) => {
+      return key.startsWith(this.state.keys as string);
+    });
     if (shown.length === 1) {
       this.activate();
       this.store.dispatch(followControllerActions.disable());
@@ -58,18 +76,18 @@ export default class FollowController {
 
     broadcastMessage(this.win, {
       type: messages.FOLLOW_SHOW_HINTS,
-      keys: this.state.keys,
+      keys: this.state.keys as string,
     });
   }
 
-  activate() {
+  activate(): void {
     broadcastMessage(this.win, {
       type: messages.FOLLOW_ACTIVATE,
-      keys: this.state.keys,
+      keys: this.state.keys as string,
     });
   }
 
-  keyPress(key, ctrlKey) {
+  keyPress(key: string, ctrlKey: boolean): boolean {
     if (key === '[' && ctrlKey) {
       this.store.dispatch(followControllerActions.disable());
       return true;
@@ -107,25 +125,28 @@ export default class FollowController {
       viewSize: { width: viewWidth, height: viewHeight },
       framePosition: { x: 0, y: 0 },
     }), '*');
-    frameElements.forEach((element) => {
-      let { left: frameX, top: frameY } = element.getBoundingClientRect();
+    frameElements.forEach((ele) => {
+      let { left: frameX, top: frameY } = ele.getBoundingClientRect();
       let message = JSON.stringify({
         type: messages.FOLLOW_REQUEST_COUNT_TARGETS,
         viewSize: { width: viewWidth, height: viewHeight },
         framePosition: { x: frameX, y: frameY },
       });
-      element.contentWindow.postMessage(message, '*');
+      if (ele instanceof HTMLFrameElement && ele.contentWindow ||
+        ele instanceof HTMLIFrameElement && ele.contentWindow) {
+        ele.contentWindow.postMessage(message, '*');
+      }
     });
   }
 
-  create(count, sender) {
+  create(count: number, sender: WebMessageSender) {
     let produced = [];
     for (let i = 0; i < count; ++i) {
-      produced.push(this.producer.produce());
+      produced.push((this.producer as HintKeyProducer).produce());
     }
     this.keys = this.keys.concat(produced);
 
-    sender.postMessage(JSON.stringify({
+    (sender as Window).postMessage(JSON.stringify({
       type: messages.FOLLOW_CREATE_HINTS,
       keysArray: produced,
       newTab: this.state.newTab,
diff --git a/src/content/components/top-content/index.ts b/src/content/components/top-content/index.ts
index 1aaef1b..ac95ea9 100644
--- a/src/content/components/top-content/index.ts
+++ b/src/content/components/top-content/index.ts
@@ -2,33 +2,43 @@ import CommonComponent from '../common';
 import FollowController from './follow-controller';
 import FindComponent from './find';
 import * as consoleFrames from '../../console-frames';
-import messages from 'shared/messages';
-import * as scrolls from 'content/scrolls';
+import * as messages from '../../../shared/messages';
+import MessageListener from '../../MessageListener';
+import * as scrolls from '../../scrolls';
 
 export default class TopContent {
+  private win: Window;
 
-  constructor(win, store) {
+  private store: any;
+
+  constructor(win: Window, store: any) {
     this.win = win;
     this.store = store;
 
     new CommonComponent(win, store); // eslint-disable-line no-new
     new FollowController(win, store); // eslint-disable-line no-new
-    new FindComponent(win, store); // eslint-disable-line no-new
+    new FindComponent(store); // eslint-disable-line no-new
 
     // TODO make component
     consoleFrames.initialize(this.win.document);
 
-    messages.onMessage(this.onMessage.bind(this));
+    new MessageListener().onWebMessage(this.onWebMessage.bind(this));
+    new MessageListener().onBackgroundMessage(
+      this.onBackgroundMessage.bind(this));
   }
 
-  onMessage(message) {
-    let addonState = this.store.getState().addon;
-
+  onWebMessage(message: messages.Message) {
     switch (message.type) {
     case messages.CONSOLE_UNFOCUS:
       this.win.focus();
       consoleFrames.blur(window.document);
-      return Promise.resolve();
+    }
+  }
+
+  onBackgroundMessage(message: messages.Message) {
+    let addonState = this.store.getState().addon;
+
+    switch (message.type) {
     case messages.ADDON_ENABLED_QUERY:
       return Promise.resolve({
         type: messages.ADDON_ENABLED_RESPONSE,
diff --git a/src/content/console-frames.ts b/src/content/console-frames.ts
index ecb5a87..bd6b835 100644
--- a/src/content/console-frames.ts
+++ b/src/content/console-frames.ts
@@ -1,6 +1,6 @@
-import messages from 'shared/messages';
+import * as messages from '../shared/messages';
 
-const initialize = (doc) => {
+const initialize = (doc: Document): HTMLIFrameElement => {
   let iframe = doc.createElement('iframe');
   iframe.src = browser.runtime.getURL('build/console.html');
   iframe.id = 'vimvixen-console-frame';
@@ -10,13 +10,13 @@ const initialize = (doc) => {
   return iframe;
 };
 
-const blur = (doc) => {
-  let iframe = doc.getElementById('vimvixen-console-frame');
-  iframe.blur();
+const blur = (doc: Document) => {
+  let ele = doc.getElementById('vimvixen-console-frame') as HTMLIFrameElement;
+  ele.blur();
 };
 
-const postError = (text) => {
-  browser.runtime.sendMessage({
+const postError = (text: string): Promise<any> => {
+  return browser.runtime.sendMessage({
     type: messages.CONSOLE_FRAME_MESSAGE,
     message: {
       type: messages.CONSOLE_SHOW_ERROR,
@@ -25,8 +25,8 @@ const postError = (text) => {
   });
 };
 
-const postInfo = (text) => {
-  browser.runtime.sendMessage({
+const postInfo = (text: string): Promise<any> => {
+  return browser.runtime.sendMessage({
     type: messages.CONSOLE_FRAME_MESSAGE,
     message: {
       type: messages.CONSOLE_SHOW_INFO,
diff --git a/src/content/focuses.ts b/src/content/focuses.ts
index a6f6cc8..8f53881 100644
--- a/src/content/focuses.ts
+++ b/src/content/focuses.ts
@@ -1,11 +1,13 @@
-import * as doms from 'shared/utils/dom';
+import * as doms from '../shared/utils/dom';
 
-const focusInput = () => {
+const focusInput = (): void => {
   let inputTypes = ['email', 'number', 'search', 'tel', 'text', 'url'];
   let inputSelector = inputTypes.map(type => `input[type=${type}]`).join(',');
   let targets = window.document.querySelectorAll(inputSelector + ',textarea');
   let target = Array.from(targets).find(doms.isVisible);
-  if (target) {
+  if (target instanceof HTMLInputElement) {
+    target.focus();
+  } else if (target instanceof HTMLTextAreaElement) {
     target.focus();
   }
 };
diff --git a/src/content/hint-key-producer.ts b/src/content/hint-key-producer.ts
index 14b23b6..935394e 100644
--- a/src/content/hint-key-producer.ts
+++ b/src/content/hint-key-producer.ts
@@ -1,5 +1,9 @@
 export default class HintKeyProducer {
-  constructor(charset) {
+  private charset: string;
+
+  private counter: number[];
+
+  constructor(charset: string) {
     if (charset.length === 0) {
       throw new TypeError('charset is empty');
     }
@@ -8,13 +12,13 @@ export default class HintKeyProducer {
     this.counter = [];
   }
 
-  produce() {
+  produce(): string {
     this.increment();
 
     return this.counter.map(x => this.charset[x]).join('');
   }
 
-  increment() {
+  private increment(): void {
     let max = this.charset.length - 1;
     if (this.counter.every(x => x === max)) {
       this.counter = new Array(this.counter.length + 1).fill(0);
diff --git a/src/content/index.ts b/src/content/index.ts
index 9edb712..309f27f 100644
--- a/src/content/index.ts
+++ b/src/content/index.ts
@@ -1,14 +1,9 @@
-import { createStore, applyMiddleware } from 'redux';
-import promise from 'redux-promise';
-import reducers from 'content/reducers';
 import TopContentComponent from './components/top-content';
 import FrameContentComponent from './components/frame-content';
 import consoleFrameStyle from './site-style';
+import { newStore } from './store';
 
-const store = createStore(
-  reducers,
-  applyMiddleware(promise),
-);
+const store = newStore();
 
 if (window.self === window.top) {
   new TopContentComponent(window, store); // eslint-disable-line no-new
diff --git a/src/content/navigates.ts b/src/content/navigates.ts
index c9baa30..a2007a6 100644
--- a/src/content/navigates.ts
+++ b/src/content/navigates.ts
@@ -1,58 +1,63 @@
-const REL_PATTERN = {
+const REL_PATTERN: {[key: string]: RegExp} = {
   prev: /^(?:prev(?:ious)?|older)\b|\u2039|\u2190|\xab|\u226a|<</i,
   next: /^(?:next|newer)\b|\u203a|\u2192|\xbb|\u226b|>>/i,
 };
 
 // Return the last element in the document matching the supplied selector
 // and the optional filter, or null if there are no matches.
-const selectLast = (win, selector, filter) => {
-  let nodes = win.document.querySelectorAll(selector);
+// eslint-disable-next-line func-style
+function selectLast<E extends Element>(
+  win: Window,
+  selector: string,
+  filter?: (e: E) => boolean,
+): E | null {
+  let nodes = Array.from(
+    win.document.querySelectorAll(selector) as NodeListOf<E>
+  );
 
   if (filter) {
-    nodes = Array.from(nodes).filter(filter);
+    nodes = nodes.filter(filter);
   }
-
   return nodes.length ? nodes[nodes.length - 1] : null;
-};
+}
 
-const historyPrev = (win) => {
+const historyPrev = (win: Window): void => {
   win.history.back();
 };
 
-const historyNext = (win) => {
+const historyNext = (win: Window): void => {
   win.history.forward();
 };
 
 // Code common to linkPrev and linkNext which navigates to the specified page.
-const linkRel = (win, rel) => {
-  let link = selectLast(win, `link[rel~=${rel}][href]`);
-
+const linkRel = (win: Window, rel: string): void => {
+  let link = selectLast<HTMLLinkElement>(win, `link[rel~=${rel}][href]`);
   if (link) {
-    win.location = link.href;
+    win.location.href = link.href;
     return;
   }
 
   const pattern = REL_PATTERN[rel];
 
-  link = selectLast(win, `a[rel~=${rel}][href]`) ||
+  let a = selectLast<HTMLAnchorElement>(win, `a[rel~=${rel}][href]`) ||
     // `innerText` is much slower than `textContent`, but produces much better
     // (i.e. less unexpected) results
     selectLast(win, 'a[href]', lnk => pattern.test(lnk.innerText));
 
-  if (link) {
-    link.click();
+  if (a) {
+    a.click();
   }
 };
 
-const linkPrev = (win) => {
+const linkPrev = (win: Window): void => {
   linkRel(win, 'prev');
 };
 
-const linkNext = (win) => {
+const linkNext = (win: Window): void => {
   linkRel(win, 'next');
 };
 
-const parent = (win) => {
+const parent = (win: Window): void => {
   const loc = win.location;
   if (loc.hash !== '') {
     loc.hash = '';
@@ -71,8 +76,8 @@ const parent = (win) => {
   }
 };
 
-const root = (win) => {
-  win.location = win.location.origin;
+const root = (win: Window): void => {
+  win.location.href = win.location.origin;
 };
 
 export { historyPrev, historyNext, linkPrev, linkNext, parent, root };
diff --git a/src/content/reducers/addon.ts b/src/content/reducers/addon.ts
index 0def55a..2131228 100644
--- a/src/content/reducers/addon.ts
+++ b/src/content/reducers/addon.ts
@@ -1,10 +1,17 @@
-import actions from 'content/actions';
+import * as actions from '../actions';
 
-const defaultState = {
+export interface State {
+  enabled: boolean;
+}
+
+const defaultState: State = {
   enabled: true,
 };
 
-export default function reducer(state = defaultState, action = {}) {
+export default function reducer(
+  state: State = defaultState,
+  action: actions.AddonAction,
+): State {
   switch (action.type) {
   case actions.ADDON_SET_ENABLED:
     return { ...state,
diff --git a/src/content/reducers/find.ts b/src/content/reducers/find.ts
index 4560e2c..8c3e637 100644
--- a/src/content/reducers/find.ts
+++ b/src/content/reducers/find.ts
@@ -1,11 +1,19 @@
-import actions from 'content/actions';
+import * as actions from '../actions';
 
-const defaultState = {
+export interface State {
+  keyword: string | null;
+  found: boolean;
+}
+
+const defaultState: State = {
   keyword: null,
   found: false,
 };
 
-export default function reducer(state = defaultState, action = {}) {
+export default function reducer(
+  state: State = defaultState,
+  action: actions.FindAction,
+): State {
   switch (action.type) {
   case actions.FIND_SET_KEYWORD:
     return { ...state,
diff --git a/src/content/reducers/follow-controller.ts b/src/content/reducers/follow-controller.ts
index 5869c47..6965704 100644
--- a/src/content/reducers/follow-controller.ts
+++ b/src/content/reducers/follow-controller.ts
@@ -1,13 +1,23 @@
-import actions from 'content/actions';
+import * as actions from '../actions';
 
-const defaultState = {
+export interface State {
+  enabled: boolean;
+  newTab: boolean;
+  background: boolean;
+  keys: string,
+}
+
+const defaultState: State = {
   enabled: false,
   newTab: false,
   background: false,
   keys: '',
 };
 
-export default function reducer(state = defaultState, action = {}) {
+export default function reducer(
+  state: State = defaultState,
+  action: actions.FollowAction,
+): State {
   switch (action.type) {
   case actions.FOLLOW_CONTROLLER_ENABLE:
     return { ...state,
diff --git a/src/content/reducers/index.ts b/src/content/reducers/index.ts
index bf612a3..fb5eb84 100644
--- a/src/content/reducers/index.ts
+++ b/src/content/reducers/index.ts
@@ -1,10 +1,20 @@
 import { combineReducers } from 'redux';
-import addon from './addon';
-import find from './find';
-import setting from './setting';
-import input from './input';
-import followController from './follow-controller';
-import mark from './mark';
+import addon, { State as AddonState } from './addon';
+import find, { State as FindState } from './find';
+import setting, { State as SettingState } from './setting';
+import input, { State as InputState } from './input';
+import followController, { State as FollowControllerState }
+  from './follow-controller';
+import mark, { State as MarkState } from './mark';
+
+export interface State {
+  addon: AddonState;
+  find: FindState;
+  setting: SettingState;
+  input: InputState;
+  followController: FollowControllerState;
+  mark: MarkState;
+}
 
 export default combineReducers({
   addon, find, setting, input, followController, mark,
diff --git a/src/content/reducers/input.ts b/src/content/reducers/input.ts
index 23e7dd2..6257e49 100644
--- a/src/content/reducers/input.ts
+++ b/src/content/reducers/input.ts
@@ -1,10 +1,17 @@
-import actions from 'content/actions';
+import * as actions from '../actions';
 
-const defaultState = {
+export interface State {
+  keys: string[];
+}
+
+const defaultState: State = {
   keys: []
 };
 
-export default function reducer(state = defaultState, action = {}) {
+export default function reducer(
+  state: State = defaultState,
+  action: actions.InputAction,
+): State {
   switch (action.type) {
   case actions.INPUT_KEY_PRESS:
     return { ...state,
diff --git a/src/content/reducers/mark.ts b/src/content/reducers/mark.ts
index 2c96cc5..e78b7b9 100644
--- a/src/content/reducers/mark.ts
+++ b/src/content/reducers/mark.ts
@@ -1,12 +1,26 @@
-import actions from 'content/actions';
+import * as actions from '../actions';
 
-const defaultState = {
+interface Mark {
+  x: number;
+  y: number;
+}
+
+export interface State {
+  setMode: boolean;
+  jumpMode: boolean;
+  marks: { [key: string]: Mark };
+}
+
+const defaultState: State = {
   setMode: false,
   jumpMode: false,
   marks: {},
 };
 
-export default function reducer(state = defaultState, action = {}) {
+export default function reducer(
+  state: State = defaultState,
+  action: actions.MarkAction,
+): State {
   switch (action.type) {
   case actions.MARK_START_SET:
     return { ...state, setMode: true };
diff --git a/src/content/reducers/setting.ts b/src/content/reducers/setting.ts
index a49db6d..fa8e8ee 100644
--- a/src/content/reducers/setting.ts
+++ b/src/content/reducers/setting.ts
@@ -1,11 +1,18 @@
-import actions from 'content/actions';
+import * as actions from '../actions';
+
+export interface State {
+  keymaps: any[];
+}
 
 const defaultState = {
   // keymaps is and arrays of key-binding pairs, which is entries of Map
   keymaps: [],
 };
 
-export default function reducer(state = defaultState, action = {}) {
+export default function reducer(
+  state: State = defaultState,
+  action: actions.SettingAction,
+): State {
   switch (action.type) {
   case actions.SETTING_SET:
     return { ...action.value };
diff --git a/src/content/scrolls.ts b/src/content/scrolls.ts
index bbf2491..6a35315 100644
--- a/src/content/scrolls.ts
+++ b/src/content/scrolls.ts
@@ -1,19 +1,19 @@
-import * as doms from 'shared/utils/dom';
+import * as doms from '../shared/utils/dom';
 
 const SCROLL_DELTA_X = 64;
 const SCROLL_DELTA_Y = 64;
 
 // dirty way to store scrolling state on globally
 let scrolling = false;
-let lastTimeoutId = null;
+let lastTimeoutId: number | null = null;
 
-const isScrollableStyle = (element) => {
+const isScrollableStyle = (element: Element): boolean => {
   let { overflowX, overflowY } = window.getComputedStyle(element);
   return !(overflowX !== 'scroll' && overflowX !== 'auto' &&
     overflowY !== 'scroll' && overflowY !== 'auto');
 };
 
-const isOverflowed = (element) => {
+const isOverflowed = (element: Element): boolean => {
   return element.scrollWidth > element.clientWidth ||
     element.scrollHeight > element.clientHeight;
 };
@@ -22,7 +22,7 @@ const isOverflowed = (element) => {
 // this method is called by each scrolling, and the returned value of this
 // method is not cached.  That does not cause performance issue because in the
 // most pages, the window is root element i,e, documentElement.
-const findScrollable = (element) => {
+const findScrollable = (element: Element): Element | null => {
   if (isScrollableStyle(element) && isOverflowed(element)) {
     return element;
   }
@@ -56,12 +56,16 @@ const resetScrolling = () => {
 };
 
 class Scroller {
-  constructor(element, smooth) {
+  private element: Element;
+
+  private smooth: boolean;
+
+  constructor(element: Element, smooth: boolean) {
     this.element = element;
     this.smooth = smooth;
   }
 
-  scrollTo(x, y) {
+  scrollTo(x: number, y: number): void {
     if (!this.smooth) {
       this.element.scrollTo(x, y);
       return;
@@ -74,13 +78,13 @@ class Scroller {
     this.prepareReset();
   }
 
-  scrollBy(x, y) {
+  scrollBy(x: number, y: number): void {
     let left = this.element.scrollLeft + x;
     let top = this.element.scrollTop + y;
     this.scrollTo(left, top);
   }
 
-  prepareReset() {
+  prepareReset(): void {
     scrolling = true;
     if (lastTimeoutId) {
       clearTimeout(lastTimeoutId);
@@ -95,7 +99,7 @@ const getScroll = () => {
   return { x: target.scrollLeft, y: target.scrollTop };
 };
 
-const scrollVertically = (count, smooth) => {
+const scrollVertically = (count: number, smooth: boolean): void => {
   let target = scrollTarget();
   let delta = SCROLL_DELTA_Y * count;
   if (scrolling) {
@@ -104,7 +108,7 @@ const scrollVertically = (count, smooth) => {
   new Scroller(target, smooth).scrollBy(0, delta);
 };
 
-const scrollHorizonally = (count, smooth) => {
+const scrollHorizonally = (count: number, smooth: boolean): void => {
   let target = scrollTarget();
   let delta = SCROLL_DELTA_X * count;
   if (scrolling) {
@@ -113,7 +117,7 @@ const scrollHorizonally = (count, smooth) => {
   new Scroller(target, smooth).scrollBy(delta, 0);
 };
 
-const scrollPages = (count, smooth) => {
+const scrollPages = (count: number, smooth: boolean): void => {
   let target = scrollTarget();
   let height = target.clientHeight;
   let delta = height * count;
@@ -123,33 +127,33 @@ const scrollPages = (count, smooth) => {
   new Scroller(target, smooth).scrollBy(0, delta);
 };
 
-const scrollTo = (x, y, smooth) => {
+const scrollTo = (x: number, y: number, smooth: boolean): void => {
   let target = scrollTarget();
   new Scroller(target, smooth).scrollTo(x, y);
 };
 
-const scrollToTop = (smooth) => {
+const scrollToTop = (smooth: boolean): void => {
   let target = scrollTarget();
   let x = target.scrollLeft;
   let y = 0;
   new Scroller(target, smooth).scrollTo(x, y);
 };
 
-const scrollToBottom = (smooth) => {
+const scrollToBottom = (smooth: boolean): void => {
   let target = scrollTarget();
   let x = target.scrollLeft;
   let y = target.scrollHeight;
   new Scroller(target, smooth).scrollTo(x, y);
 };
 
-const scrollToHome = (smooth) => {
+const scrollToHome = (smooth: boolean): void => {
   let target = scrollTarget();
   let x = 0;
   let y = target.scrollTop;
   new Scroller(target, smooth).scrollTo(x, y);
 };
 
-const scrollToEnd = (smooth) => {
+const scrollToEnd = (smooth: boolean): void => {
   let target = scrollTarget();
   let x = target.scrollWidth;
   let y = target.scrollTop;
diff --git a/src/content/store/index.ts b/src/content/store/index.ts
new file mode 100644
index 0000000..5c41744
--- /dev/null
+++ b/src/content/store/index.ts
@@ -0,0 +1,8 @@
+import promise from 'redux-promise';
+import reducers from '../reducers';
+import { createStore, applyMiddleware } from 'redux';
+
+export const newStore = () => createStore(
+  reducers,
+  applyMiddleware(promise),
+);
diff --git a/src/content/urls.ts b/src/content/urls.ts
index 6e7ea31..390efde 100644
--- a/src/content/urls.ts
+++ b/src/content/urls.ts
@@ -1,7 +1,7 @@
-import messages from 'shared/messages';
+import * as messages from '../shared/messages';
 import * as urls from '../shared/urls';
 
-const yank = (win) => {
+const yank = (win: Window) => {
   let input = win.document.createElement('input');
   win.document.body.append(input);
 
@@ -15,7 +15,7 @@ const yank = (win) => {
   input.remove();
 };
 
-const paste = (win, newTab, searchSettings) => {
+const paste = (win: Window, newTab: boolean, searchSettings: any) => {
   let textarea = win.document.createElement('textarea');
   win.document.body.append(textarea);
 
@@ -25,7 +25,7 @@ const paste = (win, newTab, searchSettings) => {
   textarea.focus();
 
   if (win.document.execCommand('paste')) {
-    let value = textarea.textContent;
+    let value = textarea.textContent as string;
     let url = urls.searchUrl(value, searchSettings);
     browser.runtime.sendMessage({
       type: messages.OPEN_URL,
diff --git a/src/shared/messages.ts b/src/shared/messages.ts
index 2bc12d8..41b0f0b 100644
--- a/src/shared/messages.ts
+++ b/src/shared/messages.ts
@@ -1,78 +1,276 @@
-type WebMessageSender = Window | MessagePort | ServiceWorker | null;
-type WebMessageListener = (msg: any, sender: WebMessageSender | null) => void;
-
-const onWebMessage = (listener: WebMessageListener) => {
-  window.addEventListener('message', (event: MessageEvent) => {
-    let sender = event.source;
-    let message = null;
-    try {
-      message = JSON.parse(event.data);
-    } catch (e) {
-      // ignore unexpected message
-      return;
-    }
-    listener(message, sender);
-  });
-};
+import * as operations from './operations';
 
-const onBackgroundMessage = (
-  listener: (msg: any, sender: browser.runtime.MessageSender,
-) => void) => {
-  browser.runtime.onMessage.addListener(listener);
-};
+export const BACKGROUND_OPERATION = 'background.operation';
 
-const onMessage = (
-  listener: (msg: any, sender: WebMessageSender | browser.runtime.MessageSender,
-) => void) => {
-  onWebMessage(listener);
-  onBackgroundMessage(listener);
-};
+export const CONSOLE_UNFOCUS = 'console.unfocus';
+export const CONSOLE_ENTER_COMMAND = 'console.enter.command';
+export const CONSOLE_ENTER_FIND = 'console.enter.find';
+export const CONSOLE_QUERY_COMPLETIONS = 'console.query.completions';
+export const CONSOLE_SHOW_COMMAND = 'console.show.command';
+export const CONSOLE_SHOW_ERROR = 'console.show.error';
+export const CONSOLE_SHOW_INFO = 'console.show.info';
+export const CONSOLE_SHOW_FIND = 'console.show.find';
+export const CONSOLE_HIDE = 'console.hide';
+
+export const FOLLOW_START = 'follow.start';
+export const FOLLOW_REQUEST_COUNT_TARGETS = 'follow.request.count.targets';
+export const FOLLOW_RESPONSE_COUNT_TARGETS = 'follow.response.count.targets';
+export const FOLLOW_CREATE_HINTS = 'follow.create.hints';
+export const FOLLOW_SHOW_HINTS = 'follow.update.hints';
+export const FOLLOW_REMOVE_HINTS = 'follow.remove.hints';
+export const FOLLOW_ACTIVATE = 'follow.activate';
+export const FOLLOW_KEY_PRESS = 'follow.key.press';
+
+export const MARK_SET_GLOBAL = 'mark.set.global';
+export const MARK_JUMP_GLOBAL = 'mark.jump.global';
+
+export const TAB_SCROLL_TO = 'tab.scroll.to';
+
+export const FIND_NEXT = 'find.next';
+export const FIND_PREV = 'find.prev';
+export const FIND_GET_KEYWORD = 'find.get.keyword';
+export const FIND_SET_KEYWORD = 'find.set.keyword';
+
+export const ADDON_ENABLED_QUERY = 'addon.enabled.query';
+export const ADDON_ENABLED_RESPONSE = 'addon.enabled.response';
+export const ADDON_TOGGLE_ENABLED = 'addon.toggle.enabled';
+
+export const OPEN_URL = 'open.url';
+
+export const SETTINGS_CHANGED = 'settings.changed';
+export const SETTINGS_QUERY = 'settings.query';
+
+export const CONSOLE_FRAME_MESSAGE = 'console.frame.message';
+
+interface BackgroundOperationMessage {
+  type: typeof BACKGROUND_OPERATION;
+  operation: operations.Operation;
+}
+
+interface ConsoleUnfocusMessage {
+  type: typeof CONSOLE_UNFOCUS;
+}
+
+interface ConsoleEnterCommandMessage {
+  type: typeof CONSOLE_ENTER_COMMAND;
+  text: string;
+}
+
+interface ConsoleEnterFindMessage {
+  type: typeof CONSOLE_ENTER_FIND;
+  text: string;
+}
+
+interface ConsoleQueryCompletionsMessage {
+  type: typeof CONSOLE_QUERY_COMPLETIONS;
+  text: string;
+}
+
+interface ConsoleShowCommandMessage {
+  type: typeof CONSOLE_SHOW_COMMAND;
+  command: string;
+}
+
+interface ConsoleShowErrorMessage {
+  type: typeof CONSOLE_SHOW_ERROR;
+  text: string;
+}
+
+interface ConsoleShowInfoMessage {
+  type: typeof CONSOLE_SHOW_INFO;
+  text: string;
+}
+
+interface ConsoleShowFindMessage {
+  type: typeof CONSOLE_SHOW_FIND;
+}
+
+interface ConsoleHideMessage {
+  type: typeof CONSOLE_HIDE;
+}
+
+interface FollowStartMessage {
+  type: typeof FOLLOW_START;
+  newTab: boolean;
+  background: boolean;
+}
+
+interface FollowRequestCountTargetsMessage {
+  type: typeof FOLLOW_REQUEST_COUNT_TARGETS;
+  viewSize: { width: number, height: number };
+  framePosition: { x: number, y: number };
+}
+
+interface FollowResponseCountTargetsMessage {
+  type: typeof FOLLOW_RESPONSE_COUNT_TARGETS;
+  count: number;
+}
+
+interface FollowCreateHintsMessage {
+  type: typeof FOLLOW_CREATE_HINTS;
+  keysArray: string[];
+  newTab: boolean;
+  background: boolean;
+}
+
+interface FollowShowHintsMessage {
+  type: typeof FOLLOW_SHOW_HINTS;
+  keys: string;
+}
+
+interface FollowRemoveHintsMessage {
+  type: typeof FOLLOW_REMOVE_HINTS;
+}
+
+interface FollowActivateMessage {
+  type: typeof FOLLOW_ACTIVATE;
+  keys: string;
+}
+
+interface FollowKeyPressMessage {
+  type: typeof FOLLOW_KEY_PRESS;
+  key: string;
+  ctrlKey: boolean;
+}
+
+interface MarkSetGlobalMessage {
+  type: typeof MARK_SET_GLOBAL;
+  key: string;
+  x: number;
+  y: number;
+}
+
+interface MarkJumpGlobalMessage {
+  type: typeof MARK_JUMP_GLOBAL;
+  key: string;
+}
+
+interface TabScrollToMessage {
+  type: typeof TAB_SCROLL_TO;
+  x: number;
+  y: number;
+}
+
+interface FindNextMessage {
+  type: typeof FIND_NEXT;
+}
+
+interface FindPrevMessage {
+  type: typeof FIND_PREV;
+}
+
+interface FindGetKeywordMessage {
+  type: typeof FIND_GET_KEYWORD;
+}
+
+interface FindSetKeywordMessage {
+  type: typeof FIND_SET_KEYWORD;
+  keyword: string;
+  found: boolean;
+}
+
+interface AddonEnabledQueryMessage {
+  type: typeof ADDON_ENABLED_QUERY;
+}
+
+interface AddonEnabledResponseMessage {
+  type: typeof ADDON_ENABLED_RESPONSE;
+  enabled: boolean;
+}
+
+interface AddonToggleEnabledMessage {
+  type: typeof ADDON_TOGGLE_ENABLED;
+}
+
+interface OpenUrlMessage {
+  type: typeof OPEN_URL;
+  url: string;
+  newTab: boolean;
+  background: boolean;
+}
+
+interface SettingsChangedMessage {
+  type: typeof SETTINGS_CHANGED;
+}
+
+interface SettingsQueryMessage {
+  type: typeof SETTINGS_QUERY;
+}
+
+interface ConsoleFrameMessageMessage {
+  type: typeof CONSOLE_FRAME_MESSAGE;
+  message: any;
+}
+
+export type Message =
+  BackgroundOperationMessage |
+  ConsoleUnfocusMessage |
+  ConsoleEnterCommandMessage |
+  ConsoleEnterFindMessage |
+  ConsoleQueryCompletionsMessage |
+  ConsoleShowCommandMessage |
+  ConsoleShowErrorMessage |
+  ConsoleShowInfoMessage |
+  ConsoleShowFindMessage |
+  ConsoleHideMessage |
+  FollowStartMessage |
+  FollowRequestCountTargetsMessage |
+  FollowResponseCountTargetsMessage |
+  FollowCreateHintsMessage |
+  FollowShowHintsMessage |
+  FollowRemoveHintsMessage |
+  FollowActivateMessage |
+  FollowKeyPressMessage |
+  MarkSetGlobalMessage |
+  MarkJumpGlobalMessage |
+  TabScrollToMessage |
+  FindNextMessage |
+  FindPrevMessage |
+  FindGetKeywordMessage |
+  FindSetKeywordMessage |
+  AddonEnabledQueryMessage |
+  AddonEnabledResponseMessage |
+  AddonToggleEnabledMessage |
+  OpenUrlMessage |
+  SettingsChangedMessage |
+  SettingsQueryMessage |
+  ConsoleFrameMessageMessage;
 
-export default {
-  BACKGROUND_OPERATION: 'background.operation',
-
-  CONSOLE_UNFOCUS: 'console.unfocus',
-  CONSOLE_ENTER_COMMAND: 'console.enter.command',
-  CONSOLE_ENTER_FIND: 'console.enter.find',
-  CONSOLE_QUERY_COMPLETIONS: 'console.query.completions',
-  CONSOLE_SHOW_COMMAND: 'console.show.command',
-  CONSOLE_SHOW_ERROR: 'console.show.error',
-  CONSOLE_SHOW_INFO: 'console.show.info',
-  CONSOLE_SHOW_FIND: 'console.show.find',
-  CONSOLE_HIDE: 'console.hide',
-
-  FOLLOW_START: 'follow.start',
-  FOLLOW_REQUEST_COUNT_TARGETS: 'follow.request.count.targets',
-  FOLLOW_RESPONSE_COUNT_TARGETS: 'follow.response.count.targets',
-  FOLLOW_CREATE_HINTS: 'follow.create.hints',
-  FOLLOW_SHOW_HINTS: 'follow.update.hints',
-  FOLLOW_REMOVE_HINTS: 'follow.remove.hints',
-  FOLLOW_ACTIVATE: 'follow.activate',
-  FOLLOW_KEY_PRESS: 'follow.key.press',
-
-  MARK_SET_GLOBAL: 'mark.set.global',
-  MARK_JUMP_GLOBAL: 'mark.jump.global',
-
-  TAB_SCROLL_TO: 'tab.scroll.to',
-
-  FIND_NEXT: 'find.next',
-  FIND_PREV: 'find.prev',
-  FIND_GET_KEYWORD: 'find.get.keyword',
-  FIND_SET_KEYWORD: 'find.set.keyword',
-
-  ADDON_ENABLED_QUERY: 'addon.enabled.query',
-  ADDON_ENABLED_RESPONSE: 'addon.enabled.response',
-  ADDON_TOGGLE_ENABLED: 'addon.toggle.enabled',
-
-  OPEN_URL: 'open.url',
-
-  SETTINGS_CHANGED: 'settings.changed',
-  SETTINGS_QUERY: 'settings.query',
-
-  WINDOW_TOP_MESSAGE: 'window.top.message',
-  CONSOLE_FRAME_MESSAGE: 'console.frame.message',
-
-  onWebMessage,
-  onBackgroundMessage,
-  onMessage,
+// eslint-disable-next-line complexity
+export const valueOf = (o: any): Message => {
+  switch (o.type) {
+  case CONSOLE_UNFOCUS:
+  case CONSOLE_ENTER_COMMAND:
+  case CONSOLE_ENTER_FIND:
+  case CONSOLE_QUERY_COMPLETIONS:
+  case CONSOLE_SHOW_COMMAND:
+  case CONSOLE_SHOW_ERROR:
+  case CONSOLE_SHOW_INFO:
+  case CONSOLE_SHOW_FIND:
+  case CONSOLE_HIDE:
+  case FOLLOW_START:
+  case FOLLOW_REQUEST_COUNT_TARGETS:
+  case FOLLOW_RESPONSE_COUNT_TARGETS:
+  case FOLLOW_CREATE_HINTS:
+  case FOLLOW_SHOW_HINTS:
+  case FOLLOW_REMOVE_HINTS:
+  case FOLLOW_ACTIVATE:
+  case FOLLOW_KEY_PRESS:
+  case MARK_SET_GLOBAL:
+  case MARK_JUMP_GLOBAL:
+  case TAB_SCROLL_TO:
+  case FIND_NEXT:
+  case FIND_PREV:
+  case FIND_GET_KEYWORD:
+  case FIND_SET_KEYWORD:
+  case ADDON_ENABLED_QUERY:
+  case ADDON_ENABLED_RESPONSE:
+  case ADDON_TOGGLE_ENABLED:
+  case OPEN_URL:
+  case SETTINGS_CHANGED:
+  case SETTINGS_QUERY:
+  case CONSOLE_FRAME_MESSAGE:
+    return o;
+  }
+  throw new Error('unknown operation type: ' + o.type);
 };
diff --git a/src/shared/operations.ts b/src/shared/operations.ts
index d59723e..cc22f75 100644
--- a/src/shared/operations.ts
+++ b/src/shared/operations.ts
@@ -1,80 +1,447 @@
-const operations: { [key: string]: string } = {
-  // Hide console, or cancel some user actions
-  CANCEL: 'cancel',
-
-  // Addons
-  ADDON_ENABLE: 'addon.enable',
-  ADDON_DISABLE: 'addon.disable',
-  ADDON_TOGGLE_ENABLED: 'addon.toggle.enabled',
-
-  // Command
-  COMMAND_SHOW: 'command.show',
-  COMMAND_SHOW_OPEN: 'command.show.open',
-  COMMAND_SHOW_TABOPEN: 'command.show.tabopen',
-  COMMAND_SHOW_WINOPEN: 'command.show.winopen',
-  COMMAND_SHOW_BUFFER: 'command.show.buffer',
-  COMMAND_SHOW_ADDBOOKMARK: 'command.show.addbookmark',
-
-  // Scrolls
-  SCROLL_VERTICALLY: 'scroll.vertically',
-  SCROLL_HORIZONALLY: 'scroll.horizonally',
-  SCROLL_PAGES: 'scroll.pages',
-  SCROLL_TOP: 'scroll.top',
-  SCROLL_BOTTOM: 'scroll.bottom',
-  SCROLL_HOME: 'scroll.home',
-  SCROLL_END: 'scroll.end',
-
-  // Follows
-  FOLLOW_START: 'follow.start',
-
-  // Navigations
-  NAVIGATE_HISTORY_PREV: 'navigate.history.prev',
-  NAVIGATE_HISTORY_NEXT: 'navigate.history.next',
-  NAVIGATE_LINK_PREV: 'navigate.link.prev',
-  NAVIGATE_LINK_NEXT: 'navigate.link.next',
-  NAVIGATE_PARENT: 'navigate.parent',
-  NAVIGATE_ROOT: 'navigate.root',
-
-  // Focus
-  FOCUS_INPUT: 'focus.input',
-
-  // Page
-  PAGE_SOURCE: 'page.source',
-  PAGE_HOME: 'page.home',
-
-  // Tabs
-  TAB_CLOSE: 'tabs.close',
-  TAB_CLOSE_FORCE: 'tabs.close.force',
-  TAB_CLOSE_RIGHT: 'tabs.close.right',
-  TAB_REOPEN: 'tabs.reopen',
-  TAB_PREV: 'tabs.prev',
-  TAB_NEXT: 'tabs.next',
-  TAB_FIRST: 'tabs.first',
-  TAB_LAST: 'tabs.last',
-  TAB_PREV_SEL: 'tabs.prevsel',
-  TAB_RELOAD: 'tabs.reload',
-  TAB_PIN: 'tabs.pin',
-  TAB_UNPIN: 'tabs.unpin',
-  TAB_TOGGLE_PINNED: 'tabs.pin.toggle',
-  TAB_DUPLICATE: 'tabs.duplicate',
-
-  // Zooms
-  ZOOM_IN: 'zoom.in',
-  ZOOM_OUT: 'zoom.out',
-  ZOOM_NEUTRAL: 'zoom.neutral',
-
-  // Url yank/paste
-  URLS_YANK: 'urls.yank',
-  URLS_PASTE: 'urls.paste',
-
-  // Find
-  FIND_START: 'find.start',
-  FIND_NEXT: 'find.next',
-  FIND_PREV: 'find.prev',
-
-  // Mark
-  MARK_SET_PREFIX: 'mark.set.prefix',
-  MARK_JUMP_PREFIX: 'mark.jump.prefix',
+// Hide console; or cancel some user actions
+export const CANCEL = 'cancel';
+
+// Addons
+export const ADDON_ENABLE = 'addon.enable';
+export const ADDON_DISABLE = 'addon.disable';
+export const ADDON_TOGGLE_ENABLED = 'addon.toggle.enabled';
+
+// Command
+export const COMMAND_SHOW = 'command.show';
+export const COMMAND_SHOW_OPEN = 'command.show.open';
+export const COMMAND_SHOW_TABOPEN = 'command.show.tabopen';
+export const COMMAND_SHOW_WINOPEN = 'command.show.winopen';
+export const COMMAND_SHOW_BUFFER = 'command.show.buffer';
+export const COMMAND_SHOW_ADDBOOKMARK = 'command.show.addbookmark';
+
+// Scrolls
+export const SCROLL_VERTICALLY = 'scroll.vertically';
+export const SCROLL_HORIZONALLY = 'scroll.horizonally';
+export const SCROLL_PAGES = 'scroll.pages';
+export const SCROLL_TOP = 'scroll.top';
+export const SCROLL_BOTTOM = 'scroll.bottom';
+export const SCROLL_HOME = 'scroll.home';
+export const SCROLL_END = 'scroll.end';
+
+// Follows
+export const FOLLOW_START = 'follow.start';
+
+// Navigations
+export const NAVIGATE_HISTORY_PREV = 'navigate.history.prev';
+export const NAVIGATE_HISTORY_NEXT = 'navigate.history.next';
+export const NAVIGATE_LINK_PREV = 'navigate.link.prev';
+export const NAVIGATE_LINK_NEXT = 'navigate.link.next';
+export const NAVIGATE_PARENT = 'navigate.parent';
+export const NAVIGATE_ROOT = 'navigate.root';
+
+// Focus
+export const FOCUS_INPUT = 'focus.input';
+
+// Page
+export const PAGE_SOURCE = 'page.source';
+export const PAGE_HOME = 'page.home';
+
+// Tabs
+export const TAB_CLOSE = 'tabs.close';
+export const TAB_CLOSE_FORCE = 'tabs.close.force';
+export const TAB_CLOSE_RIGHT = 'tabs.close.right';
+export const TAB_REOPEN = 'tabs.reopen';
+export const TAB_PREV = 'tabs.prev';
+export const TAB_NEXT = 'tabs.next';
+export const TAB_FIRST = 'tabs.first';
+export const TAB_LAST = 'tabs.last';
+export const TAB_PREV_SEL = 'tabs.prevsel';
+export const TAB_RELOAD = 'tabs.reload';
+export const TAB_PIN = 'tabs.pin';
+export const TAB_UNPIN = 'tabs.unpin';
+export const TAB_TOGGLE_PINNED = 'tabs.pin.toggle';
+export const TAB_DUPLICATE = 'tabs.duplicate';
+
+// Zooms
+export const ZOOM_IN = 'zoom.in';
+export const ZOOM_OUT = 'zoom.out';
+export const ZOOM_NEUTRAL = 'zoom.neutral';
+
+// Url yank/paste
+export const URLS_YANK = 'urls.yank';
+export const URLS_PASTE = 'urls.paste';
+
+// Find
+export const FIND_START = 'find.start';
+export const FIND_NEXT = 'find.next';
+export const FIND_PREV = 'find.prev';
+
+// Mark
+export const MARK_SET_PREFIX = 'mark.set.prefix';
+export const MARK_JUMP_PREFIX = 'mark.jump.prefix';
+
+export interface CancelOperation {
+  type: typeof CANCEL;
+}
+
+export interface AddonEnableOperation {
+  type: typeof ADDON_ENABLE;
+}
+
+export interface AddonDisableOperation {
+  type: typeof ADDON_DISABLE;
+}
+
+export interface AddonToggleEnabledOperation {
+  type: typeof ADDON_TOGGLE_ENABLED;
+}
+
+export interface CommandShowOperation {
+  type: typeof COMMAND_SHOW;
+}
+
+export interface CommandShowOpenOperation {
+  type: typeof COMMAND_SHOW_OPEN;
+  alter: boolean;
+}
+
+export interface CommandShowTabopenOperation {
+  type: typeof COMMAND_SHOW_TABOPEN;
+  alter: boolean;
+}
+
+export interface CommandShowWinopenOperation {
+  type: typeof COMMAND_SHOW_WINOPEN;
+  alter: boolean;
+}
+
+export interface CommandShowBufferOperation {
+  type: typeof COMMAND_SHOW_BUFFER;
+}
+
+export interface CommandShowAddbookmarkOperation {
+  type: typeof COMMAND_SHOW_ADDBOOKMARK;
+  alter: boolean;
+}
+
+export interface ScrollVerticallyOperation {
+  type: typeof SCROLL_VERTICALLY;
+  count: number;
+}
+
+export interface ScrollHorizonallyOperation {
+  type: typeof SCROLL_HORIZONALLY;
+  count: number;
+}
+
+export interface ScrollPagesOperation {
+  type: typeof SCROLL_PAGES;
+  count: number;
+}
+
+export interface ScrollTopOperation {
+  type: typeof SCROLL_TOP;
+}
+
+export interface ScrollBottomOperation {
+  type: typeof SCROLL_BOTTOM;
+}
+
+export interface ScrollHomeOperation {
+  type: typeof SCROLL_HOME;
+}
+
+export interface ScrollEndOperation {
+  type: typeof SCROLL_END;
+}
+
+export interface FollowStartOperation {
+  type: typeof FOLLOW_START;
+  newTab: boolean;
+  background: boolean;
+}
+
+export interface NavigateHistoryPrevOperation {
+  type: typeof NAVIGATE_HISTORY_PREV;
+}
+
+export interface NavigateHistoryNextOperation {
+  type: typeof NAVIGATE_HISTORY_NEXT;
+}
+
+export interface NavigateLinkPrevOperation {
+  type: typeof NAVIGATE_LINK_PREV;
+}
+
+export interface NavigateLinkNextOperation {
+  type: typeof NAVIGATE_LINK_NEXT;
+}
+
+export interface NavigateParentOperation {
+  type: typeof NAVIGATE_PARENT;
+}
+
+export interface NavigateRootOperation {
+  type: typeof NAVIGATE_ROOT;
+}
+
+export interface FocusInputOperation {
+  type: typeof FOCUS_INPUT;
+}
+
+export interface PageSourceOperation {
+  type: typeof PAGE_SOURCE;
+}
+
+export interface PageHomeOperation {
+  type: typeof PAGE_HOME;
+  newTab: boolean;
+}
+
+export interface TabCloseOperation {
+  type: typeof TAB_CLOSE;
+}
+
+export interface TabCloseForceOperation {
+  type: typeof TAB_CLOSE_FORCE;
+}
+
+export interface TabCloseRightOperation {
+  type: typeof TAB_CLOSE_RIGHT;
+}
+
+export interface TabReopenOperation {
+  type: typeof TAB_REOPEN;
+}
+
+export interface TabPrevOperation {
+  type: typeof TAB_PREV;
+}
+
+export interface TabNextOperation {
+  type: typeof TAB_NEXT;
+}
+
+export interface TabFirstOperation {
+  type: typeof TAB_FIRST;
+}
+
+export interface TabLastOperation {
+  type: typeof TAB_LAST;
+}
+
+export interface TabPrevSelOperation {
+  type: typeof TAB_PREV_SEL;
+}
+
+export interface TabReloadOperation {
+  type: typeof TAB_RELOAD;
+  cache: boolean;
+}
+
+export interface TabPinOperation {
+  type: typeof TAB_PIN;
+}
+
+export interface TabUnpinOperation {
+  type: typeof TAB_UNPIN;
+}
+
+export interface TabTogglePinnedOperation {
+  type: typeof TAB_TOGGLE_PINNED;
+}
+
+export interface TabDuplicateOperation {
+  type: typeof TAB_DUPLICATE;
+}
+
+export interface ZoomInOperation {
+  type: typeof ZOOM_IN;
+}
+
+export interface ZoomOutOperation {
+  type: typeof ZOOM_OUT;
+}
+
+export interface ZoomNeutralOperation {
+  type: typeof ZOOM_NEUTRAL;
+}
+
+export interface UrlsYankOperation {
+  type: typeof URLS_YANK;
+}
+
+export interface UrlsPasteOperation {
+  type: typeof URLS_PASTE;
+  newTab: boolean;
+}
+
+export interface FindStartOperation {
+  type: typeof FIND_START;
+}
+
+export interface FindNextOperation {
+  type: typeof FIND_NEXT;
+}
+
+export interface FindPrevOperation {
+  type: typeof FIND_PREV;
+}
+
+export interface MarkSetPrefixOperation {
+  type: typeof MARK_SET_PREFIX;
+}
+
+export interface MarkJumpPrefixOperation {
+  type: typeof MARK_JUMP_PREFIX;
+}
+
+export type Operation =
+  CancelOperation |
+  AddonEnableOperation |
+  AddonDisableOperation |
+  AddonToggleEnabledOperation |
+  CommandShowOperation |
+  CommandShowOpenOperation |
+  CommandShowTabopenOperation |
+  CommandShowWinopenOperation |
+  CommandShowBufferOperation |
+  CommandShowAddbookmarkOperation |
+  ScrollVerticallyOperation |
+  ScrollHorizonallyOperation |
+  ScrollPagesOperation |
+  ScrollTopOperation |
+  ScrollBottomOperation |
+  ScrollHomeOperation |
+  ScrollEndOperation |
+  FollowStartOperation |
+  NavigateHistoryPrevOperation |
+  NavigateHistoryNextOperation |
+  NavigateLinkPrevOperation |
+  NavigateLinkNextOperation |
+  NavigateParentOperation |
+  NavigateRootOperation |
+  FocusInputOperation |
+  PageSourceOperation |
+  PageHomeOperation |
+  TabCloseOperation |
+  TabCloseForceOperation |
+  TabCloseRightOperation |
+  TabReopenOperation |
+  TabPrevOperation |
+  TabNextOperation |
+  TabFirstOperation |
+  TabLastOperation |
+  TabPrevSelOperation |
+  TabReloadOperation |
+  TabPinOperation |
+  TabUnpinOperation |
+  TabTogglePinnedOperation |
+  TabDuplicateOperation |
+  ZoomInOperation |
+  ZoomOutOperation |
+  ZoomNeutralOperation |
+  UrlsYankOperation |
+  UrlsPasteOperation |
+  FindStartOperation |
+  FindNextOperation |
+  FindPrevOperation |
+  MarkSetPrefixOperation |
+  MarkJumpPrefixOperation;
+
+const assertOptionalBoolean = (obj: any, name: string) => {
+  if (Object.prototype.hasOwnProperty.call(obj, name) &&
+      typeof obj[name] !== 'boolean') {
+    throw new TypeError(`Not a boolean parameter '${name}'`);
+  }
+};
+
+const assertRequiredNumber = (obj: any, name: string) => {
+  if (!Object.prototype.hasOwnProperty.call(obj, name) ||
+    typeof obj[name] !== 'number') {
+    throw new TypeError(`Missing number parameter '${name}`);
+  }
 };
 
-export default operations;
+// eslint-disable-next-line complexity, max-lines-per-function
+export const valueOf = (o: any): Operation => {
+  if (!Object.prototype.hasOwnProperty.call(o, 'type')) {
+    throw new TypeError(`missing 'type' field`);
+  }
+  switch (o.type) {
+  case COMMAND_SHOW_OPEN:
+  case COMMAND_SHOW_TABOPEN:
+  case COMMAND_SHOW_WINOPEN:
+  case COMMAND_SHOW_ADDBOOKMARK:
+    assertOptionalBoolean(o, 'alter');
+    return { type: o.type, alter: Boolean(o.alter) };
+  case SCROLL_VERTICALLY:
+  case SCROLL_HORIZONALLY:
+  case SCROLL_PAGES:
+    assertRequiredNumber(o, 'count');
+    return { type: o.type, count: Number(o.count) };
+  case FOLLOW_START:
+    assertOptionalBoolean(o, 'newTab');
+    assertOptionalBoolean(o, 'background');
+    return {
+      type: FOLLOW_START,
+      newTab: Boolean(typeof o.newTab === undefined ? false : o.newTab),
+      background: Boolean(typeof o.background === undefined ? true : o.background), // eslint-disable-line max-len
+    };
+  case PAGE_HOME:
+    assertOptionalBoolean(o, 'newTab');
+    return {
+      type: PAGE_HOME,
+      newTab: Boolean(typeof o.newTab === undefined ? false : o.newTab),
+    };
+  case TAB_RELOAD:
+    assertOptionalBoolean(o, 'cache');
+    return {
+      type: TAB_RELOAD,
+      cache: Boolean(typeof o.cache === undefined ? false : o.cache),
+    };
+  case URLS_PASTE:
+    assertOptionalBoolean(o, 'newTab');
+    return {
+      type: URLS_PASTE,
+      newTab: Boolean(typeof o.newTab === undefined ? false : o.newTab),
+    };
+  case CANCEL:
+  case ADDON_ENABLE:
+  case ADDON_DISABLE:
+  case ADDON_TOGGLE_ENABLED:
+  case COMMAND_SHOW:
+  case COMMAND_SHOW_BUFFER:
+  case SCROLL_TOP:
+  case SCROLL_BOTTOM:
+  case SCROLL_HOME:
+  case SCROLL_END:
+  case NAVIGATE_HISTORY_PREV:
+  case NAVIGATE_HISTORY_NEXT:
+  case NAVIGATE_LINK_PREV:
+  case NAVIGATE_LINK_NEXT:
+  case NAVIGATE_PARENT:
+  case NAVIGATE_ROOT:
+  case FOCUS_INPUT:
+  case PAGE_SOURCE:
+  case TAB_CLOSE:
+  case TAB_CLOSE_FORCE:
+  case TAB_CLOSE_RIGHT:
+  case TAB_REOPEN:
+  case TAB_PREV:
+  case TAB_NEXT:
+  case TAB_FIRST:
+  case TAB_LAST:
+  case TAB_PREV_SEL:
+  case TAB_PIN:
+  case TAB_UNPIN:
+  case TAB_TOGGLE_PINNED:
+  case TAB_DUPLICATE:
+  case ZOOM_IN:
+  case ZOOM_OUT:
+  case ZOOM_NEUTRAL:
+  case URLS_YANK:
+  case FIND_START:
+  case FIND_NEXT:
+  case FIND_PREV:
+  case MARK_SET_PREFIX:
+  case MARK_JUMP_PREFIX:
+    return { type: o.type };
+  }
+  throw new Error('unknown operation type: ' + o.type);
+};
diff --git a/src/shared/settings/validator.ts b/src/shared/settings/validator.ts
index 0483931..71cc466 100644
--- a/src/shared/settings/validator.ts
+++ b/src/shared/settings/validator.ts
@@ -1,4 +1,4 @@
-import operations from '../operations';
+import * as operations from '../operations';
 import * as properties from './properties';
 
 const VALID_TOP_KEYS = ['keymaps', 'search', 'blacklist', 'properties'];
diff --git a/src/shared/utils/keys.ts b/src/shared/utils/keys.ts
index d9abef7..e9b0365 100644
--- a/src/shared/utils/keys.ts
+++ b/src/shared/utils/keys.ts
@@ -1,4 +1,4 @@
-interface Key {
+export interface Key {
     key: string;
     shiftKey: boolean | undefined;
     ctrlKey: boolean | undefined;
diff --git a/test/content/actions/follow-controller.test.ts b/test/content/actions/follow-controller.test.ts
index 718a90a..a4b1710 100644
--- a/test/content/actions/follow-controller.test.ts
+++ b/test/content/actions/follow-controller.test.ts
@@ -1,4 +1,4 @@
-import actions from 'content/actions';
+import * as actions from 'content/actions';
 import * as followControllerActions from 'content/actions/follow-controller';
 
 describe('follow-controller actions', () => {
diff --git a/test/content/actions/input.test.ts b/test/content/actions/input.test.ts
index fe9db5f..33238a5 100644
--- a/test/content/actions/input.test.ts
+++ b/test/content/actions/input.test.ts
@@ -1,4 +1,4 @@
-import actions from 'content/actions';
+import * as actions from 'content/actions';
 import * as inputActions from 'content/actions/input';
 
 describe("input actions", () => {
diff --git a/test/content/actions/mark.test.ts b/test/content/actions/mark.test.ts
index adbf06b..6c6d59e 100644
--- a/test/content/actions/mark.test.ts
+++ b/test/content/actions/mark.test.ts
@@ -1,4 +1,4 @@
-import actions from 'content/actions';
+import * as actions from 'content/actions';
 import * as markActions from 'content/actions/mark';
 
 describe('mark actions', () => {
diff --git a/test/content/actions/setting.test.ts b/test/content/actions/setting.test.ts
index 10f6807..0721d5d 100644
--- a/test/content/actions/setting.test.ts
+++ b/test/content/actions/setting.test.ts
@@ -1,4 +1,4 @@
-import actions from 'content/actions';
+import * as actions from 'content/actions';
 import * as settingActions from 'content/actions/setting';
 
 describe("setting actions", () => {
diff --git a/test/content/components/common/input.test.ts b/test/content/components/common/input.test.ts
index 2ba5507..f3a943c 100644
--- a/test/content/components/common/input.test.ts
+++ b/test/content/components/common/input.test.ts
@@ -21,12 +21,14 @@ describe('InputComponent', () => {
         ++b;
       }
     });
-    component.onKeyDown({ key: 'a' });
-    component.onKeyDown({ key: 'b' });
-    component.onKeyPress({ key: 'a' });
-    component.onKeyUp({ key: 'a' });
-    component.onKeyPress({ key: 'b' });
-    component.onKeyUp({ key: 'b' });
+
+    let elem = document.body;
+    component.onKeyDown({ key: 'a', target: elem });
+    component.onKeyDown({ key: 'b', target: elem });
+    component.onKeyPress({ key: 'a', target: elem });
+    component.onKeyUp({ key: 'a', target: elem });
+    component.onKeyPress({ key: 'b', target: elem });
+    component.onKeyUp({ key: 'b', target: elem });
 
     expect(a).is.equals(1);
     expect(b).is.equals(1);
diff --git a/test/content/reducers/addon.test.ts b/test/content/reducers/addon.test.ts
index d4eb845..fb05244 100644
--- a/test/content/reducers/addon.test.ts
+++ b/test/content/reducers/addon.test.ts
@@ -1,4 +1,4 @@
-import actions from 'content/actions';
+import * as actions from 'content/actions';
 import addonReducer from 'content/reducers/addon';
 
 describe("addon reducer", () => {
diff --git a/test/content/reducers/find.test.ts b/test/content/reducers/find.test.ts
index a8c30d7..66a2c67 100644
--- a/test/content/reducers/find.test.ts
+++ b/test/content/reducers/find.test.ts
@@ -1,4 +1,4 @@
-import actions from 'content/actions';
+import * as actions from 'content/actions';
 import findReducer from 'content/reducers/find';
 
 describe("find reducer", () => {
diff --git a/test/content/reducers/follow-controller.test.ts b/test/content/reducers/follow-controller.test.ts
index 8a4c2d4..39f326c 100644
--- a/test/content/reducers/follow-controller.test.ts
+++ b/test/content/reducers/follow-controller.test.ts
@@ -1,4 +1,4 @@
-import actions from 'content/actions';
+import * as actions from 'content/actions';
 import followControllerReducer from 'content/reducers/follow-controller';
 
 describe('follow-controller reducer', () => {
diff --git a/test/content/reducers/input.test.ts b/test/content/reducers/input.test.ts
index 0011943..f892201 100644
--- a/test/content/reducers/input.test.ts
+++ b/test/content/reducers/input.test.ts
@@ -1,4 +1,4 @@
-import actions from 'content/actions';
+import * as actions from 'content/actions';
 import inputReducer from 'content/reducers/input';
 
 describe("input reducer", () => {
diff --git a/test/content/reducers/mark.test.ts b/test/content/reducers/mark.test.ts
index 76efbf7..1a51c3e 100644
--- a/test/content/reducers/mark.test.ts
+++ b/test/content/reducers/mark.test.ts
@@ -1,4 +1,4 @@
-import actions from 'content/actions';
+import * as actions from 'content/actions';
 import reducer from 'content/reducers/mark';
 
 describe("mark reducer", () => {
diff --git a/test/content/reducers/setting.test.ts b/test/content/reducers/setting.test.ts
index 4e4c095..226fc58 100644
--- a/test/content/reducers/setting.test.ts
+++ b/test/content/reducers/setting.test.ts
@@ -1,4 +1,4 @@
-import actions from 'content/actions';
+import * as actions from 'content/actions';
 import settingReducer from 'content/reducers/setting';
 
 describe("content setting reducer", () => {
diff --git a/test/shared/operations.test.ts b/test/shared/operations.test.ts
new file mode 100644
index 0000000..42a3eed
--- /dev/null
+++ b/test/shared/operations.test.ts
@@ -0,0 +1,41 @@
+import * as operations from 'shared/operations';
+
+describe('operations', () => {
+  describe('#valueOf', () => {
+    it('returns an Operation', () => {
+      let op: operations.Operation = operations.valueOf({
+        type: operations.SCROLL_VERTICALLY,
+        count: 10,
+      });
+      expect(op.type).to.equal(operations.SCROLL_VERTICALLY);
+      expect(op.count).to.equal(10);
+    });
+
+    it('throws an Error on missing required parameter', () => {
+      expect(() => operations.valueOf({
+        type: operations.SCROLL_VERTICALLY,
+      })).to.throw(TypeError);
+    });
+
+    it('fills default valus of optional parameter', () => {
+      let op: operations.Operation = operations.valueOf({
+        type: operations.COMMAND_SHOW_OPEN,
+      });
+
+      expect(op.type).to.equal(operations.COMMAND_SHOW_OPEN)
+      expect(op.alter).to.be.false;
+    });
+
+    it('throws an Error on mismatch of parameter', () => {
+      expect(() => operations.valueOf({
+        type: operations.SCROLL_VERTICALLY,
+        count: '10',
+      })).to.throw(TypeError);
+
+      expect(() => valueOf({
+        type: operations.COMMAND_SHOW_OPEN,
+        alter: 'true',
+      })).to.throw(TypeError);
+    });
+  });
+})
diff --git a/tsconfig.json b/tsconfig.json
index 575601b..b61ee23 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -2,11 +2,11 @@
   "compilerOptions": {
     "target": "es2017",
     "module": "commonjs",
+    "lib": ["es6", "dom", "es2017"],
     "allowJs": true,
     "checkJs": true,
+    "noEmit": true,
     "jsx": "react",
-    "declaration": true,
-    "declarationMap": true,
     "sourceMap": true,
     "outDir": "./build",
     "removeComments": true,
@@ -29,5 +29,8 @@
     "esModuleInterop": true,
 
     "typeRoots": ["node_modules/@types", "node_modules/web-ext-types"]
-  }
+  },
+  "include": [
+    "src"
+  ]
 }
-- 
cgit v1.2.3


From a0882bbceb7ed71d56bf8557620449fbc3f19749 Mon Sep 17 00:00:00 2001
From: Shin'ya Ueoka <ueokande@i-beam.org>
Date: Sun, 5 May 2019 08:03:29 +0900
Subject: Declare setting types

---
 src/background/controllers/SettingController.ts    |   3 +-
 src/background/domains/GlobalMark.ts               |   3 +-
 src/background/domains/Setting.ts                  |  59 ---
 .../repositories/PersistentSettingRepository.ts    |   6 +-
 src/background/repositories/SettingRepository.ts   |  34 +-
 src/background/usecases/CommandUseCase.ts          |   7 +-
 src/background/usecases/CompletionsUseCase.ts      |  30 +-
 src/background/usecases/SettingUseCase.ts          |  17 +-
 src/background/usecases/parsers.ts                 |  21 +-
 src/content/actions/index.ts                       |   3 +-
 src/content/actions/operation.ts                   |   4 +-
 src/content/actions/setting.ts                     |  23 +-
 src/content/components/common/index.ts             |   5 +-
 src/content/components/common/input.ts             |   1 -
 src/content/components/common/keymapper.ts         |  28 +-
 src/content/components/common/mark.ts              |  10 +-
 .../components/top-content/follow-controller.ts    |   4 +-
 src/content/reducers/setting.ts                    |  24 +-
 src/settings/actions/index.ts                      |  16 +-
 src/settings/actions/setting.ts                    |  60 +--
 src/settings/components/form/KeymapsForm.tsx       |  23 +-
 src/settings/components/form/SearchForm.tsx        |  30 +-
 src/settings/components/index.tsx                  | 104 ++++--
 src/settings/keymaps.ts                            |   3 -
 src/settings/reducers/setting.ts                   |  22 +-
 src/settings/storage.ts                            |  15 +
 src/shared/SettingData.ts                          | 414 +++++++++++++++++++++
 src/shared/Settings.ts                             | 200 ++++++++++
 src/shared/operations.ts                           |   2 +-
 src/shared/properties.ts                           |  50 +++
 src/shared/property-defs.ts                        |  50 +++
 src/shared/settings/default.ts                     |  85 -----
 src/shared/settings/properties.ts                  |  24 --
 src/shared/settings/storage.ts                     |  32 --
 src/shared/settings/validator.ts                   |  76 ----
 src/shared/settings/values.ts                      | 108 ------
 test/background/usecases/parsers.test.ts           |  41 +-
 test/content/actions/setting.test.ts               |  46 ++-
 test/content/reducers/setting.test.ts              |  21 +-
 test/settings/components/form/KeymapsForm.test.tsx |  14 +-
 .../components/form/SearchEngineForm.test.tsx      |  60 ++-
 test/settings/reducers/setting.test.ts             |   3 +-
 test/shared/SettingData.test.ts                    | 293 +++++++++++++++
 test/shared/Settings.test.ts                       | 190 ++++++++++
 test/shared/properties.test.js                     |  18 +
 test/shared/property-defs.test.js                  |  18 +
 test/shared/settings/validator.test.ts             |  81 ----
 test/shared/settings/values.test.ts                | 138 -------
 48 files changed, 1617 insertions(+), 902 deletions(-)
 delete mode 100644 src/background/domains/Setting.ts
 create mode 100644 src/settings/storage.ts
 create mode 100644 src/shared/SettingData.ts
 create mode 100644 src/shared/Settings.ts
 create mode 100644 src/shared/properties.ts
 create mode 100644 src/shared/property-defs.ts
 delete mode 100644 src/shared/settings/default.ts
 delete mode 100644 src/shared/settings/properties.ts
 delete mode 100644 src/shared/settings/storage.ts
 delete mode 100644 src/shared/settings/validator.ts
 delete mode 100644 src/shared/settings/values.ts
 create mode 100644 test/shared/SettingData.test.ts
 create mode 100644 test/shared/Settings.test.ts
 create mode 100644 test/shared/properties.test.js
 create mode 100644 test/shared/property-defs.test.js
 delete mode 100644 test/shared/settings/validator.test.ts
 delete mode 100644 test/shared/settings/values.test.ts

(limited to 'src/content/components/top-content')

diff --git a/src/background/controllers/SettingController.ts b/src/background/controllers/SettingController.ts
index f8b7302..dfd2817 100644
--- a/src/background/controllers/SettingController.ts
+++ b/src/background/controllers/SettingController.ts
@@ -1,5 +1,6 @@
 import SettingUseCase from '../usecases/SettingUseCase';
 import ContentMessageClient from '../infrastructures/ContentMessageClient';
+import Settings from '../../shared/Settings';
 
 export default class SettingController {
   private settingUseCase: SettingUseCase;
@@ -11,7 +12,7 @@ export default class SettingController {
     this.contentMessageClient = new ContentMessageClient();
   }
 
-  getSetting(): any {
+  getSetting(): Promise<Settings> {
     return this.settingUseCase.get();
   }
 
diff --git a/src/background/domains/GlobalMark.ts b/src/background/domains/GlobalMark.ts
index 0964373..1ae912e 100644
--- a/src/background/domains/GlobalMark.ts
+++ b/src/background/domains/GlobalMark.ts
@@ -1,6 +1,7 @@
-export interface GlobalMark {
+export default interface GlobalMark {
   readonly tabId: number;
   readonly url: string;
   readonly x: number;
   readonly y: number;
+  // eslint-disable-next-line semi
 }
diff --git a/src/background/domains/Setting.ts b/src/background/domains/Setting.ts
deleted file mode 100644
index b2b1ff2..0000000
--- a/src/background/domains/Setting.ts
+++ /dev/null
@@ -1,59 +0,0 @@
-import DefaultSettings from '../../shared/settings/default';
-import * as settingsValues from '../../shared/settings/values';
-
-type SettingValue = {
-    source: string,
-    json: string,
-    form: any
-}
-
-export default class Setting {
-  private obj: SettingValue;
-
-  constructor({ source, json, form }: SettingValue) {
-    this.obj = {
-      source, json, form
-    };
-  }
-
-  get source(): string {
-    return this.obj.source;
-  }
-
-  get json(): string {
-    return this.obj.json;
-  }
-
-  get form(): any {
-    return this.obj.form;
-  }
-
-  value() {
-    let value = JSON.parse(DefaultSettings.json);
-    if (this.obj.source === 'json') {
-      value = settingsValues.valueFromJson(this.obj.json);
-    } else if (this.obj.source === 'form') {
-      value = settingsValues.valueFromForm(this.obj.form);
-    }
-    if (!value.properties) {
-      value.properties = {};
-    }
-    return { ...settingsValues.valueFromJson(DefaultSettings.json), ...value };
-  }
-
-  serialize(): SettingValue {
-    return this.obj;
-  }
-
-  static deserialize(obj: SettingValue): Setting {
-    return new Setting({ source: obj.source, json: obj.json, form: obj.form });
-  }
-
-  static defaultSettings() {
-    return new Setting({
-      source: DefaultSettings.source,
-      json: DefaultSettings.json,
-      form: {},
-    });
-  }
-}
diff --git a/src/background/repositories/PersistentSettingRepository.ts b/src/background/repositories/PersistentSettingRepository.ts
index 3f2f4a1..18476fd 100644
--- a/src/background/repositories/PersistentSettingRepository.ts
+++ b/src/background/repositories/PersistentSettingRepository.ts
@@ -1,12 +1,12 @@
-import Setting from '../domains/Setting';
+import SettingData from '../../shared/SettingData';
 
 export default class SettingRepository {
-  async load(): Promise<any> {
+  async load(): Promise<SettingData | null> {
     let { settings } = await browser.storage.local.get('settings');
     if (!settings) {
       return null;
     }
-    return Setting.deserialize(settings);
+    return SettingData.valueOf(settings);
   }
 }
 
diff --git a/src/background/repositories/SettingRepository.ts b/src/background/repositories/SettingRepository.ts
index 15355ba..eb83a2c 100644
--- a/src/background/repositories/SettingRepository.ts
+++ b/src/background/repositories/SettingRepository.ts
@@ -1,4 +1,6 @@
 import MemoryStorage from '../infrastructures/MemoryStorage';
+import Settings from '../../shared/Settings';
+import * as PropertyDefs from '../../shared/property-defs';
 
 const CACHED_SETTING_KEY = 'setting';
 
@@ -9,17 +11,41 @@ export default class SettingRepository {
     this.cache = new MemoryStorage();
   }
 
-  get(): Promise<any> {
+  get(): Promise<Settings> {
     return Promise.resolve(this.cache.get(CACHED_SETTING_KEY));
   }
 
-  update(value: any): any {
+  update(value: Settings): void {
     return this.cache.set(CACHED_SETTING_KEY, value);
   }
 
-  async setProperty(name: string, value: string): Promise<any> {
+  async setProperty(
+    name: string, value: string | number | boolean,
+  ): Promise<void> {
+    let def = PropertyDefs.defs.find(d => name === d.name);
+    if (!def) {
+      throw new Error('unknown property: ' + name);
+    }
+    if (typeof value !== def.type) {
+      throw new TypeError(`property type of ${name} mismatch: ${typeof value}`);
+    }
+    let newValue = value;
+    if (typeof value === 'string' && value === '') {
+      newValue = def.defaultValue;
+    }
+
     let current = await this.get();
-    current.properties[name] = value;
+    switch (name) {
+    case 'hintchars':
+      current.properties.hintchars = newValue as string;
+      break;
+    case 'smoothscroll':
+      current.properties.smoothscroll = newValue as boolean;
+      break;
+    case 'complete':
+      current.properties.complete = newValue as string;
+      break;
+    }
     return this.update(current);
   }
 }
diff --git a/src/background/usecases/CommandUseCase.ts b/src/background/usecases/CommandUseCase.ts
index e0e3ada..2247d7b 100644
--- a/src/background/usecases/CommandUseCase.ts
+++ b/src/background/usecases/CommandUseCase.ts
@@ -6,7 +6,6 @@ import SettingRepository from '../repositories/SettingRepository';
 import BookmarkRepository from '../repositories/BookmarkRepository';
 import ConsoleClient from '../infrastructures/ConsoleClient';
 import ContentMessageClient from '../infrastructures/ContentMessageClient';
-import * as properties from '../../shared/settings/properties';
 
 export default class CommandIndicator {
   private tabPresenter: TabPresenter;
@@ -115,16 +114,16 @@ export default class CommandIndicator {
 
   async addbookmark(title: string): Promise<any> {
     let tab = await this.tabPresenter.getCurrent();
-    let item = await this.bookmarkRepository.create(title, tab.url);
+    let item = await this.bookmarkRepository.create(title, tab.url as string);
     let message = 'Saved current page: ' + item.url;
-    return this.consoleClient.showInfo(tab.id, message);
+    return this.consoleClient.showInfo(tab.id as number, message);
   }
 
   async set(keywords: string): Promise<any> {
     if (keywords.length === 0) {
       return;
     }
-    let [name, value] = parsers.parseSetOption(keywords, properties.types);
+    let [name, value] = parsers.parseSetOption(keywords);
     await this.settingRepository.setProperty(name, value);
 
     return this.contentMessageClient.broadcastSettingsChanged();
diff --git a/src/background/usecases/CompletionsUseCase.ts b/src/background/usecases/CompletionsUseCase.ts
index f3a808b..ae1ceed 100644
--- a/src/background/usecases/CompletionsUseCase.ts
+++ b/src/background/usecases/CompletionsUseCase.ts
@@ -4,7 +4,7 @@ import CompletionsRepository from '../repositories/CompletionsRepository';
 import * as filters from './filters';
 import SettingRepository from '../repositories/SettingRepository';
 import TabPresenter from '../presenters/TabPresenter';
-import * as properties from '../../shared/settings/properties';
+import * as PropertyDefs from '../../shared/property-defs';
 
 const COMPLETION_ITEM_LIMIT = 10;
 
@@ -44,7 +44,7 @@ export default class CompletionsUseCase {
     let settings = await this.settingRepository.get();
     let groups: CompletionGroup[] = [];
 
-    let complete = settings.properties.complete || properties.defaults.complete;
+    let complete = settings.properties.complete;
     for (let c of complete) {
       if (c === 's') {
         // eslint-disable-next-line no-await-in-loop
@@ -127,25 +127,25 @@ export default class CompletionsUseCase {
   }
 
   querySet(name: string, keywords: string): Promise<CompletionGroup[]> {
-    let items = Object.keys(properties.docs).map((key) => {
-      if (properties.types[key] === 'boolean') {
+    let items = PropertyDefs.defs.map((def) => {
+      if (def.type === 'boolean') {
         return [
           {
-            caption: key,
-            content: name + ' ' + key,
-            url: 'Enable ' + properties.docs[key],
+            caption: def.name,
+            content: name + ' ' + def.name,
+            url: 'Enable ' + def.description,
           }, {
-            caption: 'no' + key,
-            content: name + ' no' + key,
-            url: 'Disable ' + properties.docs[key],
+            caption: 'no' + def.name,
+            content: name + ' no' + def.name,
+            url: 'Disable ' + def.description
           }
         ];
       }
       return [
         {
-          caption: key,
-          content: name + ' ' + key,
-          url: 'Set ' + properties.docs[key],
+          caption: def.name,
+          content: name + ' ' + def.name,
+          url: 'Set ' + def.description,
         }
       ];
     });
@@ -195,8 +195,8 @@ export default class CompletionsUseCase {
       .map(filters.filterByTailingSlash)
       .map(pages => filters.filterByPathname(pages, COMPLETION_ITEM_LIMIT))
       .map(pages => filters.filterByOrigin(pages, COMPLETION_ITEM_LIMIT))[0]
-      .sort((x: HistoryItem, y: HistoryItem) => {
-        return Number(x.visitCount) < Number(y.visitCount);
+      .sort((x: HistoryItem, y: HistoryItem): number => {
+        return Number(x.visitCount) - Number(y.visitCount);
       })
       .slice(0, COMPLETION_ITEM_LIMIT);
     return histories.map(page => ({
diff --git a/src/background/usecases/SettingUseCase.ts b/src/background/usecases/SettingUseCase.ts
index b66ce02..aa3b534 100644
--- a/src/background/usecases/SettingUseCase.ts
+++ b/src/background/usecases/SettingUseCase.ts
@@ -1,7 +1,8 @@
-import Setting from '../domains/Setting';
 // eslint-disable-next-line max-len
 import PersistentSettingRepository from '../repositories/PersistentSettingRepository';
 import SettingRepository from '../repositories/SettingRepository';
+import { DefaultSettingData } from '../../shared/SettingData';
+import Settings from '../../shared/Settings';
 
 export default class SettingUseCase {
   private persistentSettingRepository: PersistentSettingRepository;
@@ -13,20 +14,18 @@ export default class SettingUseCase {
     this.settingRepository = new SettingRepository();
   }
 
-  get(): Promise<any> {
+  get(): Promise<Settings> {
     return this.settingRepository.get();
   }
 
-  async reload(): Promise<any> {
-    let settings = await this.persistentSettingRepository.load();
-    if (!settings) {
-      settings = Setting.defaultSettings();
+  async reload(): Promise<Settings> {
+    let data = await this.persistentSettingRepository.load();
+    if (!data) {
+      data = DefaultSettingData;
     }
 
-    let value = settings.value();
-
+    let value = data.toSettings();
     this.settingRepository.update(value);
-
     return value;
   }
 }
diff --git a/src/background/usecases/parsers.ts b/src/background/usecases/parsers.ts
index 3616ac3..6135fd8 100644
--- a/src/background/usecases/parsers.ts
+++ b/src/background/usecases/parsers.ts
@@ -1,3 +1,5 @@
+import * as PropertyDefs from '../../shared//property-defs';
+
 const mustNumber = (v: any): number => {
   let num = Number(v);
   if (isNaN(num)) {
@@ -7,29 +9,28 @@ const mustNumber = (v: any): number => {
 };
 
 const parseSetOption = (
-  word: string,
-  types: { [key: string]: string },
+  args: string,
 ): any[] => {
-  let [key, value]: any[] = word.split('=');
+  let [key, value]: any[] = args.split('=');
   if (value === undefined) {
     value = !key.startsWith('no');
     key = value ? key : key.slice(2);
   }
-  let type = types[key];
-  if (!type) {
+  let def = PropertyDefs.defs.find(d => d.name === key);
+  if (!def) {
     throw new Error('Unknown property: ' + key);
   }
-  if (type === 'boolean' && typeof value !== 'boolean' ||
-       type !== 'boolean' && typeof value === 'boolean') {
-    throw new Error('Invalid argument: ' + word);
+  if (def.type === 'boolean' && typeof value !== 'boolean' ||
+       def.type !== 'boolean' && typeof value === 'boolean') {
+    throw new Error('Invalid argument: ' + args);
   }
 
-  switch (type) {
+  switch (def.type) {
   case 'string': return [key, value];
   case 'number': return [key, mustNumber(value)];
   case 'boolean': return [key, value];
   }
-  throw new Error('Unknown property type: ' + type);
+  throw new Error('Unknown property type: ' + def.type);
 };
 
 export { parseSetOption };
diff --git a/src/content/actions/index.ts b/src/content/actions/index.ts
index 18d0a69..a259211 100644
--- a/src/content/actions/index.ts
+++ b/src/content/actions/index.ts
@@ -1,4 +1,5 @@
 import Redux from 'redux';
+import Settings from '../../shared/Settings';
 
 // Enable/disable
 export const ADDON_SET_ENABLED = 'addon.set.enabled';
@@ -45,7 +46,7 @@ export interface FindSetKeywordAction extends Redux.Action {
 
 export interface SettingSetAction extends Redux.Action {
   type: typeof SETTING_SET;
-  value: any;
+  settings: Settings,
 }
 
 export interface InputKeyPressAction extends Redux.Action {
diff --git a/src/content/actions/operation.ts b/src/content/actions/operation.ts
index 6acb407..41e080b 100644
--- a/src/content/actions/operation.ts
+++ b/src/content/actions/operation.ts
@@ -8,7 +8,6 @@ import * as urls from '../urls';
 import * as consoleFrames from '../console-frames';
 import * as addonActions from './addon';
 import * as markActions from './mark';
-import * as properties from '../../shared/settings/properties';
 
 // eslint-disable-next-line complexity, max-lines-per-function
 const exec = (
@@ -16,8 +15,7 @@ const exec = (
   settings: any,
   addonEnabled: boolean,
 ): Promise<actions.Action> | actions.Action => {
-  let smoothscroll = settings.properties.smoothscroll ||
-    properties.defaults.smoothscroll;
+  let smoothscroll = settings.properties.smoothscroll;
   switch (operation.type) {
   case operations.ADDON_ENABLE:
     return addonActions.enable();
diff --git a/src/content/actions/setting.ts b/src/content/actions/setting.ts
index a8f049a..92f8559 100644
--- a/src/content/actions/setting.ts
+++ b/src/content/actions/setting.ts
@@ -1,29 +1,20 @@
 import * as actions from './index';
-import * as keyUtils from '../../shared/utils/keys';
 import * as operations from '../../shared/operations';
 import * as messages from '../../shared/messages';
+import Settings, { Keymaps } from '../../shared/Settings';
 
-const reservedKeymaps = {
+const reservedKeymaps: Keymaps = {
   '<Esc>': { type: operations.CANCEL },
   '<C-[>': { type: operations.CANCEL },
 };
 
-const set = (value: any): actions.SettingAction => {
-  let entries: any[] = [];
-  if (value.keymaps) {
-    let keymaps = { ...value.keymaps, ...reservedKeymaps };
-    entries = Object.entries(keymaps).map((entry) => {
-      return [
-        keyUtils.fromMapKeys(entry[0]),
-        entry[1],
-      ];
-    });
-  }
-
+const set = (settings: Settings): actions.SettingAction => {
   return {
     type: actions.SETTING_SET,
-    value: { ...value,
-      keymaps: entries, }
+    settings: {
+      ...settings,
+      keymaps: { ...settings.keymaps, ...reservedKeymaps },
+    }
   };
 };
 
diff --git a/src/content/components/common/index.ts b/src/content/components/common/index.ts
index 9b5164e..8bd697f 100644
--- a/src/content/components/common/index.ts
+++ b/src/content/components/common/index.ts
@@ -8,6 +8,7 @@ import MessageListener from '../../MessageListener';
 import * as addonActions from '../../actions/addon';
 import * as blacklists from '../../../shared/blacklists';
 import * as keys from '../../../shared/utils/keys';
+import * as actions from '../../actions';
 
 export default class Common {
   private win: Window;
@@ -45,9 +46,9 @@ export default class Common {
   reloadSettings() {
     try {
       this.store.dispatch(settingActions.load())
-        .then(({ value: settings }: any) => {
+        .then((action: actions.SettingAction) => {
           let enabled = !blacklists.includes(
-            settings.blacklist, this.win.location.href
+            action.settings.blacklist, this.win.location.href
           );
           this.store.dispatch(addonActions.setEnabled(enabled));
         });
diff --git a/src/content/components/common/input.ts b/src/content/components/common/input.ts
index 64eb5f3..1fe34c9 100644
--- a/src/content/components/common/input.ts
+++ b/src/content/components/common/input.ts
@@ -61,7 +61,6 @@ export default class InputComponent {
     }
 
     let key = keys.fromKeyboardEvent(e);
-
     for (let listener of this.onKeyListeners) {
       let stop = listener(key);
       if (stop) {
diff --git a/src/content/components/common/keymapper.ts b/src/content/components/common/keymapper.ts
index d9c9834..c94bae0 100644
--- a/src/content/components/common/keymapper.ts
+++ b/src/content/components/common/keymapper.ts
@@ -3,7 +3,10 @@ import * as operationActions from '../../actions/operation';
 import * as operations from '../../../shared/operations';
 import * as keyUtils from '../../../shared/utils/keys';
 
-const mapStartsWith = (mapping, keys) => {
+const mapStartsWith = (
+  mapping: keyUtils.Key[],
+  keys: keyUtils.Key[],
+): boolean => {
   if (mapping.length < keys.length) {
     return false;
   }
@@ -16,26 +19,33 @@ const mapStartsWith = (mapping, keys) => {
 };
 
 export default class KeymapperComponent {
-  constructor(store) {
+  private store: any;
+
+  constructor(store: any) {
     this.store = store;
   }
 
   // eslint-disable-next-line max-statements
-  key(key) {
+  key(key: keyUtils.Key): boolean {
     this.store.dispatch(inputActions.keyPress(key));
 
     let state = this.store.getState();
     let input = state.input;
-    let keymaps = new Map(state.setting.keymaps);
+    let keymaps = new Map<keyUtils.Key[], operations.Operation>(
+      state.setting.keymaps.map(
+        (e: {key: keyUtils.Key[], op: operations.Operation}) => [e.key, e.op],
+      )
+    );
 
-    let matched = Array.from(keymaps.keys()).filter((mapping) => {
-      return mapStartsWith(mapping, input.keys);
-    });
+    let matched = Array.from(keymaps.keys()).filter(
+      (mapping: keyUtils.Key[]) => {
+        return mapStartsWith(mapping, input.keys);
+      });
     if (!state.addon.enabled) {
       // available keymaps are only ADDON_ENABLE and ADDON_TOGGLE_ENABLED if
       // the addon disabled
       matched = matched.filter((keys) => {
-        let type = keymaps.get(keys).type;
+        let type = (keymaps.get(keys) as operations.Operation).type;
         return type === operations.ADDON_ENABLE ||
           type === operations.ADDON_TOGGLE_ENABLED;
       });
@@ -47,7 +57,7 @@ export default class KeymapperComponent {
       matched.length === 1 && input.keys.length < matched[0].length) {
       return true;
     }
-    let operation = keymaps.get(matched[0]);
+    let operation = keymaps.get(matched[0]) as operations.Operation;
     let act = operationActions.exec(
       operation, state.setting, state.addon.enabled
     );
diff --git a/src/content/components/common/mark.ts b/src/content/components/common/mark.ts
index 500d03b..686116c 100644
--- a/src/content/components/common/mark.ts
+++ b/src/content/components/common/mark.ts
@@ -1,7 +1,6 @@
-import * as markActions from 'content/actions/mark';
-import * as scrolls from 'content/scrolls';
-import * as consoleFrames from 'content/console-frames';
-import * as properties from 'shared/settings/properties';
+import * as markActions from '../../actions/mark';
+import * as scrolls from '../..//scrolls';
+import * as consoleFrames from '../..//console-frames';
 
 const cancelKey = (key): boolean => {
   return key.key === 'Esc' || key.key === '[' && key.ctrlKey;
@@ -20,8 +19,7 @@ export default class MarkComponent {
   // eslint-disable-next-line max-statements
   key(key) {
     let { mark: markStage, setting } = this.store.getState();
-    let smoothscroll = setting.properties.smoothscroll ||
-      properties.defaults.smoothscroll;
+    let smoothscroll = setting.properties.smoothscroll;
 
     if (!markStage.setMode && !markStage.jumpMode) {
       return false;
diff --git a/src/content/components/top-content/follow-controller.ts b/src/content/components/top-content/follow-controller.ts
index be71f6e..d49b22a 100644
--- a/src/content/components/top-content/follow-controller.ts
+++ b/src/content/components/top-content/follow-controller.ts
@@ -2,7 +2,6 @@ import * as followControllerActions from '../../actions/follow-controller';
 import * as messages from '../../../shared/messages';
 import MessageListener, { WebMessageSender } from '../../MessageListener';
 import HintKeyProducer from '../../hint-key-producer';
-import * as properties from '../../../shared/settings/properties';
 
 const broadcastMessage = (win: Window, message: messages.Message): void => {
   let json = JSON.stringify(message);
@@ -162,7 +161,6 @@ export default class FollowController {
   }
 
   hintchars() {
-    return this.store.getState().setting.properties.hintchars ||
-      properties.defaults.hintchars;
+    return this.store.getState().setting.properties.hintchars;
   }
 }
diff --git a/src/content/reducers/setting.ts b/src/content/reducers/setting.ts
index fa8e8ee..a3dc3aa 100644
--- a/src/content/reducers/setting.ts
+++ b/src/content/reducers/setting.ts
@@ -1,12 +1,20 @@
 import * as actions from '../actions';
+import * as keyUtils from '../../shared/utils/keys';
+import * as operations from '../../shared/operations';
+import { Properties } from '../../shared/Settings';
 
 export interface State {
-  keymaps: any[];
+  keymaps: { key: keyUtils.Key[], op: operations.Operation }[];
+  properties: Properties;
 }
 
-const defaultState = {
-  // keymaps is and arrays of key-binding pairs, which is entries of Map
+const defaultState: State = {
   keymaps: [],
+  properties: {
+    complete: '',
+    smoothscroll: false,
+    hintchars: '',
+  },
 };
 
 export default function reducer(
@@ -15,7 +23,15 @@ export default function reducer(
 ): State {
   switch (action.type) {
   case actions.SETTING_SET:
-    return { ...action.value };
+    return {
+      keymaps: Object.entries(action.settings.keymaps).map((entry) => {
+        return {
+          key: keyUtils.fromMapKeys(entry[0]),
+          op: entry[1],
+        };
+      }),
+      properties: action.settings.properties,
+    };
   default:
     return state;
   }
diff --git a/src/settings/actions/index.ts b/src/settings/actions/index.ts
index 75c6bb5..b1e996e 100644
--- a/src/settings/actions/index.ts
+++ b/src/settings/actions/index.ts
@@ -1,3 +1,7 @@
+import {
+  JSONSettings, FormSettings, SettingSource,
+} from '../../shared/SettingData';
+
 // Settings
 export const SETTING_SET_SETTINGS = 'setting.set.settings';
 export const SETTING_SHOW_ERROR = 'setting.show.error';
@@ -6,25 +10,25 @@ export const SETTING_SWITCH_TO_JSON = 'setting.switch.to.json';
 
 interface SettingSetSettingsAcion {
   type: typeof SETTING_SET_SETTINGS;
-  source: string;
-  json: string;
-  form: any;
+  source: SettingSource;
+  json?: JSONSettings;
+  form?: FormSettings;
 }
 
 interface SettingShowErrorAction {
   type: typeof SETTING_SHOW_ERROR;
   error: string;
-  json: string;
+  json: JSONSettings;
 }
 
 interface SettingSwitchToFormAction {
   type: typeof SETTING_SWITCH_TO_FORM;
-  form: any;
+  form: FormSettings,
 }
 
 interface SettingSwitchToJsonAction {
   type: typeof SETTING_SWITCH_TO_JSON;
-  json: string;
+  json: JSONSettings,
 }
 
 export type SettingAction =
diff --git a/src/settings/actions/setting.ts b/src/settings/actions/setting.ts
index b03cd80..9eb416e 100644
--- a/src/settings/actions/setting.ts
+++ b/src/settings/actions/setting.ts
@@ -1,35 +1,35 @@
 import * as actions from './index';
-import * as validator from '../../shared/settings/validator';
-import * as settingsValues from '../../shared/settings/values';
-import * as settingsStorage from '../../shared/settings/storage';
-import keymaps from '../keymaps';
+import * as storages from '../storage';
+import SettingData, {
+  JSONSettings, FormSettings, SettingSource,
+} from '../../shared/SettingData';
 
 const load = async(): Promise<actions.SettingAction> => {
-  let settings = await settingsStorage.loadRaw();
-  return set(settings);
+  let data = await storages.load();
+  return set(data);
 };
 
-const save = async(settings: any): Promise<actions.SettingAction> => {
+const save = async(data: SettingData): Promise<actions.SettingAction> => {
   try {
-    if (settings.source === 'json') {
-      let value = JSON.parse(settings.json);
-      validator.validate(value);
+    if (data.getSource() === SettingSource.JSON) {
+      // toSettings exercise validation
+      data.toSettings();
     }
   } catch (e) {
     return {
       type: actions.SETTING_SHOW_ERROR,
       error: e.toString(),
-      json: settings.json,
+      json: data.getJSON(),
     };
   }
-  await settingsStorage.save(settings);
-  return set(settings);
+  await storages.save(data);
+  return set(data);
 };
 
-const switchToForm = (json: string): actions.SettingAction => {
+const switchToForm = (json: JSONSettings): actions.SettingAction => {
   try {
-    validator.validate(JSON.parse(json));
-    let form = settingsValues.formFromJson(json, keymaps.allowedOps);
+    // toSettings exercise validation
+    let form = FormSettings.fromSettings(json.toSettings());
     return {
       type: actions.SETTING_SWITCH_TO_FORM,
       form,
@@ -43,21 +43,31 @@ const switchToForm = (json: string): actions.SettingAction => {
   }
 };
 
-const switchToJson = (form: any): actions.SettingAction => {
-  let json = settingsValues.jsonFromForm(form);
+const switchToJson = (form: FormSettings): actions.SettingAction => {
+  let json = JSONSettings.fromSettings(form.toSettings());
   return {
     type: actions.SETTING_SWITCH_TO_JSON,
     json,
   };
 };
 
-const set = (settings: any): actions.SettingAction => {
-  return {
-    type: actions.SETTING_SET_SETTINGS,
-    source: settings.source,
-    json: settings.json,
-    form: settings.form,
-  };
+const set = (data: SettingData): actions.SettingAction => {
+  let source = data.getSource();
+  switch (source) {
+  case SettingSource.JSON:
+    return {
+      type: actions.SETTING_SET_SETTINGS,
+      source: source,
+      json: data.getJSON(),
+    };
+  case SettingSource.Form:
+    return {
+      type: actions.SETTING_SET_SETTINGS,
+      source: source,
+      form: data.getForm(),
+    };
+  }
+  throw new Error(`unknown source: ${source}`);
 };
 
 export { load, save, set, switchToForm, switchToJson };
diff --git a/src/settings/components/form/KeymapsForm.tsx b/src/settings/components/form/KeymapsForm.tsx
index ab44464..ad4d0e7 100644
--- a/src/settings/components/form/KeymapsForm.tsx
+++ b/src/settings/components/form/KeymapsForm.tsx
@@ -2,32 +2,30 @@ import './KeymapsForm.scss';
 import React from 'react';
 import Input from '../ui/Input';
 import keymaps from '../../keymaps';
+import { FormKeymaps } from '../../../shared/SettingData';
 
-type Value = {[key: string]: string};
-
-interface Props{
-  value: Value;
-  onChange: (e: Value) => void;
+interface Props {
+  value: FormKeymaps;
+  onChange: (e: FormKeymaps) => void;
   onBlur: () => void;
 }
 
 class KeymapsForm extends React.Component<Props> {
   public static defaultProps: Props = {
-    value: {},
+    value: FormKeymaps.valueOf({}),
     onChange: () => {},
     onBlur: () => {},
   }
 
   render() {
+    let values = this.props.value.toJSON();
     return <div className='form-keymaps-form'>
       {
         keymaps.fields.map((group, index) => {
           return <div key={index} className='form-keymaps-form-field-group'>
             {
-              group.map((field) => {
-                let name = field[0];
-                let label = field[1];
-                let value = this.props.value[name] || '';
+              group.map(([name, label]) => {
+                let value = values[name] || '';
                 return <Input
                   type='text' id={name} name={name} key={name}
                   label={label} value={value}
@@ -43,10 +41,7 @@ class KeymapsForm extends React.Component<Props> {
   }
 
   bindValue(name: string, value: string) {
-    let next = { ...this.props.value };
-    next[name] = value;
-
-    this.props.onChange(next);
+    this.props.onChange(this.props.value.buildWithOverride(name, value));
   }
 }
 
diff --git a/src/settings/components/form/SearchForm.tsx b/src/settings/components/form/SearchForm.tsx
index 737e291..67dbeba 100644
--- a/src/settings/components/form/SearchForm.tsx
+++ b/src/settings/components/form/SearchForm.tsx
@@ -2,31 +2,23 @@ import './SearchForm.scss';
 import React from 'react';
 import AddButton from '../ui/AddButton';
 import DeleteButton from '../ui/DeleteButton';
-
-interface Value {
-  default: string;
-  engines: string[][];
-}
+import { FormSearch } from '../../../shared/SettingData';
 
 interface Props {
-  value: Value;
-  onChange: (value: Value) => void;
+  value: FormSearch;
+  onChange: (value: FormSearch) => void;
   onBlur: () => void;
 }
 
 class SearchForm extends React.Component<Props> {
   public static defaultProps: Props = {
-    value: { default: '', engines: []},
+    value: FormSearch.valueOf({ default: '', engines: []}),
     onChange: () => {},
     onBlur: () => {},
   }
 
   render() {
-    let value = this.props.value;
-    if (!value.engines) {
-      value.engines = [];
-    }
-
+    let value = this.props.value.toJSON();
     return <div className='form-search-form'>
       <div className='form-search-form-header'>
         <div className='column-name'>Name</div>
@@ -63,28 +55,28 @@ class SearchForm extends React.Component<Props> {
   }
 
   bindValue(e: any) {
-    let value = this.props.value;
+    let value = this.props.value.toJSON();
     let name = e.target.name;
     let index = Number(e.target.getAttribute('data-index'));
-    let next: Value = {
+    let next: typeof value = {
       default: value.default,
-      engines: value.engines ? value.engines.slice() : [],
+      engines: value.engines.slice(),
     };
 
     if (name === 'name') {
       next.engines[index][0] = e.target.value;
-      next.default = this.props.value.engines[index][0];
+      next.default = value.engines[index][0];
     } else if (name === 'url') {
       next.engines[index][1] = e.target.value;
     } else if (name === 'default') {
-      next.default = this.props.value.engines[index][0];
+      next.default = value.engines[index][0];
     } else if (name === 'add') {
       next.engines.push(['', '']);
     } else if (name === 'delete') {
       next.engines.splice(index, 1);
     }
 
-    this.props.onChange(next);
+    this.props.onChange(FormSearch.valueOf(next));
     if (name === 'delete' || name === 'default') {
       this.props.onBlur();
     }
diff --git a/src/settings/components/index.tsx b/src/settings/components/index.tsx
index f56e93f..b4a0866 100644
--- a/src/settings/components/index.tsx
+++ b/src/settings/components/index.tsx
@@ -6,22 +6,26 @@ import SearchForm from './form/SearchForm';
 import KeymapsForm from './form/KeymapsForm';
 import BlacklistForm from './form/BlacklistForm';
 import PropertiesForm from './form/PropertiesForm';
-import * as properties from '../../shared/settings/properties';
 import * as settingActions from '../../settings/actions/setting';
+import SettingData, {
+  JSONSettings, FormKeymaps, FormSearch, FormSettings,
+} from '../../shared/SettingData';
+import { State as AppState } from '../reducers/setting';
+import * as settings from '../../shared/Settings';
+import * as PropertyDefs from '../../shared/property-defs';
 
 const DO_YOU_WANT_TO_CONTINUE =
   'Some settings in JSON can be lost when migrating.  ' +
   'Do you want to continue?';
 
-interface Props {
-  source: string;
-  json: string;
-  form: any;
-  error: string;
-
+type StateProps = ReturnType<typeof mapStateToProps>;
+interface DispatchProps {
+  dispatch: (action: any) => void,
+}
+type Props = StateProps & DispatchProps & {
   // FIXME
   store: any;
-}
+};
 
 class SettingsComponent extends React.Component<Props> {
   componentDidMount() {
@@ -29,12 +33,17 @@ class SettingsComponent extends React.Component<Props> {
   }
 
   renderFormFields(form: any) {
+    let types = PropertyDefs.defs.reduce(
+      (o: {[key: string]: string}, def) => {
+        o[def.name] = def.type;
+        return o;
+      }, {});
     return <div>
       <fieldset>
         <legend>Keybindings</legend>
         <KeymapsForm
           value={form.keymaps}
-          onChange={value => this.bindForm('keymaps', value)}
+          onChange={this.bindKeymapsForm.bind(this)}
           onBlur={this.save.bind(this)}
         />
       </fieldset>
@@ -42,7 +51,7 @@ class SettingsComponent extends React.Component<Props> {
         <legend>Search Engines</legend>
         <SearchForm
           value={form.search}
-          onChange={value => this.bindForm('search', value)}
+          onChange={this.bindSearchForm.bind(this)}
           onBlur={this.save.bind(this)}
         />
       </fieldset>
@@ -50,23 +59,23 @@ class SettingsComponent extends React.Component<Props> {
         <legend>Blacklist</legend>
         <BlacklistForm
           value={form.blacklist}
-          onChange={value => this.bindForm('blacklist', value)}
+          onChange={this.bindBlacklistForm.bind(this)}
           onBlur={this.save.bind(this)}
         />
       </fieldset>
       <fieldset>
         <legend>Properties</legend>
         <PropertiesForm
-          types={properties.types}
+          types={types}
           value={form.properties}
-          onChange={value => this.bindForm('properties', value)}
+          onChange={this.bindPropertiesForm.bind(this)}
           onBlur={this.save.bind(this)}
         />
       </fieldset>
     </div>;
   }
 
-  renderJsonFields(json: string, error: string) {
+  renderJsonFields(json: JSONSettings, error: string) {
     return <div>
       <Input
         type='textarea'
@@ -76,7 +85,7 @@ class SettingsComponent extends React.Component<Props> {
         error={error}
         onValueChange={this.bindJson.bind(this)}
         onBlur={this.save.bind(this)}
-        value={json}
+        value={json.toJSON()}
       />
     </div>;
   }
@@ -87,7 +96,8 @@ class SettingsComponent extends React.Component<Props> {
     if (this.props.source === 'form') {
       fields = this.renderFormFields(this.props.form);
     } else if (this.props.source === 'json') {
-      fields = this.renderJsonFields(this.props.json, this.props.error);
+      fields = this.renderJsonFields(
+        this.props.json as JSONSettings, this.props.error);
     }
     return (
       <div>
@@ -117,45 +127,73 @@ class SettingsComponent extends React.Component<Props> {
     );
   }
 
-  bindForm(name: string, value: any) {
-    let settings = {
+  bindKeymapsForm(value: FormKeymaps) {
+    let data = new SettingData({
+      source: this.props.source,
+      form: (this.props.form as FormSettings).buildWithKeymaps(value),
+    });
+    this.props.dispatch(settingActions.set(data));
+  }
+
+  bindSearchForm(value: any) {
+    let data = new SettingData({
+      source: this.props.source,
+      form: (this.props.form as FormSettings).buildWithSearch(
+        FormSearch.valueOf(value)),
+    });
+    this.props.dispatch(settingActions.set(data));
+  }
+
+  bindBlacklistForm(value: any) {
+    let data = new SettingData({
+      source: this.props.source,
+      form: (this.props.form as FormSettings).buildWithBlacklist(
+        settings.blacklistValueOf(value)),
+    });
+    this.props.dispatch(settingActions.set(data));
+  }
+
+  bindPropertiesForm(value: any) {
+    let data = new SettingData({
       source: this.props.source,
-      json: this.props.json,
-      form: { ...this.props.form },
-    };
-    settings.form[name] = value;
-    this.props.dispatch(settingActions.set(settings));
+      form: (this.props.form as FormSettings).buildWithProperties(
+        settings.propertiesValueOf(value)),
+    });
+    this.props.dispatch(settingActions.set(data));
   }
 
   bindJson(_name: string, value: string) {
-    let settings = {
+    let data = new SettingData({
       source: this.props.source,
-      json: value,
-      form: this.props.form,
-    };
-    this.props.dispatch(settingActions.set(settings));
+      json: JSONSettings.valueOf(value),
+    });
+    this.props.dispatch(settingActions.set(data));
   }
 
   bindSource(_name: string, value: string) {
     let from = this.props.source;
     if (from === 'form' && value === 'json') {
-      this.props.dispatch(settingActions.switchToJson(this.props.form));
+      this.props.dispatch(settingActions.switchToJson(
+        this.props.form as FormSettings));
     } else if (from === 'json' && value === 'form') {
       let b = window.confirm(DO_YOU_WANT_TO_CONTINUE);
       if (!b) {
         this.forceUpdate();
         return;
       }
-      this.props.dispatch(settingActions.switchToForm(this.props.json));
+      this.props.dispatch(
+        settingActions.switchToForm(this.props.json as JSONSettings));
     }
   }
 
   save() {
-    let settings = this.props.store.getState();
-    this.props.dispatch(settingActions.save(settings));
+    let { source, json, form } = this.props.store.getState();
+    this.props.dispatch(settingActions.save(
+      new SettingData({ source, json, form }),
+    ));
   }
 }
 
-const mapStateToProps = (state: any) => state;
+const mapStateToProps = (state: AppState) => ({ ...state });
 
 export default connect(mapStateToProps)(SettingsComponent);
diff --git a/src/settings/keymaps.ts b/src/settings/keymaps.ts
index ccfc74c..38045ad 100644
--- a/src/settings/keymaps.ts
+++ b/src/settings/keymaps.ts
@@ -66,9 +66,6 @@ const fields = [
   ]
 ];
 
-const allowedOps = [].concat(...fields.map(group => group.map(e => e[0])));
-
 export default {
   fields,
-  allowedOps,
 };
diff --git a/src/settings/reducers/setting.ts b/src/settings/reducers/setting.ts
index 47c21bf..c4a21c7 100644
--- a/src/settings/reducers/setting.ts
+++ b/src/settings/reducers/setting.ts
@@ -1,23 +1,25 @@
 import * as actions from '../actions';
+import {
+  JSONSettings, FormSettings, SettingSource,
+} from '../../shared/SettingData';
 
-interface State {
-  source: string;
-  json: string;
-  form: any;
+export interface State {
+  source: SettingSource;
+  json?: JSONSettings;
+  form?: FormSettings;
   error: string;
 }
 
 const defaultState: State = {
-  source: '',
-  json: '',
-  form: null,
+  source: SettingSource.JSON,
+  json: JSONSettings.valueOf(''),
   error: '',
 };
 
 export default function reducer(
   state = defaultState,
   action: actions.SettingAction,
-) {
+): State {
   switch (action.type) {
   case actions.SETTING_SET_SETTINGS:
     return { ...state,
@@ -32,12 +34,12 @@ export default function reducer(
   case actions.SETTING_SWITCH_TO_FORM:
     return { ...state,
       error: '',
-      source: 'form',
+      source: SettingSource.Form,
       form: action.form, };
   case actions.SETTING_SWITCH_TO_JSON:
     return { ...state,
       error: '',
-      source: 'json',
+      source: SettingSource.JSON,
       json: action.json, };
   default:
     return state;
diff --git a/src/settings/storage.ts b/src/settings/storage.ts
new file mode 100644
index 0000000..748b9ab
--- /dev/null
+++ b/src/settings/storage.ts
@@ -0,0 +1,15 @@
+import SettingData, { DefaultSettingData } from '../shared/SettingData';
+
+export const load = async(): Promise<SettingData> => {
+  let { settings } = await browser.storage.local.get('settings');
+  if (!settings) {
+    return DefaultSettingData;
+  }
+  return SettingData.valueOf(settings);
+};
+
+export const save = (data: SettingData) => {
+  return browser.storage.local.set({
+    settings: data.toJSON(),
+  });
+};
diff --git a/src/shared/SettingData.ts b/src/shared/SettingData.ts
new file mode 100644
index 0000000..05e21fa
--- /dev/null
+++ b/src/shared/SettingData.ts
@@ -0,0 +1,414 @@
+import * as operations from './operations';
+import Settings, * as settings from './Settings';
+
+export class FormKeymaps {
+  private data: {[op: string]: string};
+
+  constructor(data: {[op: string]: string}) {
+    this.data = data;
+  }
+
+  toKeymaps(): settings.Keymaps {
+    let keymaps: settings.Keymaps = {};
+    for (let name of Object.keys(this.data)) {
+      let [type, argStr] = name.split('?');
+      let args = {};
+      if (argStr) {
+        args = JSON.parse(argStr);
+      }
+      let key = this.data[name];
+      keymaps[key] = operations.valueOf({ type, ...args });
+    }
+    return keymaps;
+  }
+
+  toJSON(): {[op: string]: string} {
+    return this.data;
+  }
+
+  buildWithOverride(op: string, keys: string): FormKeymaps {
+    let newData = {
+      ...this.data,
+      [op]: keys,
+    };
+    return new FormKeymaps(newData);
+  }
+
+  static valueOf(o: ReturnType<FormKeymaps['toJSON']>): FormKeymaps {
+    let data: {[op: string]: string} = {};
+    for (let op of Object.keys(o)) {
+      data[op] = o[op] as string;
+    }
+    return new FormKeymaps(data);
+  }
+
+  static fromKeymaps(keymaps: settings.Keymaps): FormKeymaps {
+    let data: {[op: string]: string} = {};
+    for (let key of Object.keys(keymaps)) {
+      let op = keymaps[key];
+      let args = { ...op };
+      delete args.type;
+
+      let name = op.type;
+      if (Object.keys(args).length > 0) {
+        name += '?' + JSON.stringify(args);
+      }
+      data[name] = key;
+    }
+    return new FormKeymaps(data);
+  }
+}
+
+export class FormSearch {
+  private default: string;
+
+  private engines: string[][];
+
+  constructor(defaultEngine: string, engines: string[][]) {
+    this.default = defaultEngine;
+    this.engines = engines;
+  }
+
+  toSearchSettings(): settings.Search {
+    return {
+      default: this.default,
+      engines: this.engines.reduce(
+        (o: {[key: string]: string}, [name, url]) => {
+          o[name] = url;
+          return o;
+        }, {}),
+    };
+  }
+
+  toJSON(): {
+    default: string;
+    engines: string[][];
+    } {
+    return {
+      default: this.default,
+      engines: this.engines,
+    };
+  }
+
+  static valueOf(o: ReturnType<FormSearch['toJSON']>): FormSearch {
+    if (!Object.prototype.hasOwnProperty.call(o, 'default')) {
+      throw new TypeError(`"default" field not set`);
+    }
+    if (!Object.prototype.hasOwnProperty.call(o, 'engines')) {
+      throw new TypeError(`"engines" field not set`);
+    }
+    return new FormSearch(o.default, o.engines);
+  }
+
+  static fromSearch(search: settings.Search): FormSearch {
+    let engines = Object.entries(search.engines).reduce(
+      (o: string[][], [name, url]) => {
+        return o.concat([[name, url]]);
+      }, []);
+    return new FormSearch(search.default, engines);
+  }
+}
+
+export class JSONSettings {
+  private json: string;
+
+  constructor(json: any) {
+    this.json = json;
+  }
+
+  toSettings(): Settings {
+    return settings.valueOf(JSON.parse(this.json));
+  }
+
+  toJSON(): string {
+    return this.json;
+  }
+
+  static valueOf(o: ReturnType<JSONSettings['toJSON']>): JSONSettings {
+    return new JSONSettings(o);
+  }
+
+  static fromSettings(data: Settings): JSONSettings {
+    return new JSONSettings(JSON.stringify(data, undefined, 2));
+  }
+}
+
+export class FormSettings {
+  private keymaps: FormKeymaps;
+
+  private search: FormSearch;
+
+  private properties: settings.Properties;
+
+  private blacklist: string[];
+
+  constructor(
+    keymaps: FormKeymaps,
+    search: FormSearch,
+    properties: settings.Properties,
+    blacklist: string[],
+  ) {
+    this.keymaps = keymaps;
+    this.search = search;
+    this.properties = properties;
+    this.blacklist = blacklist;
+  }
+
+  buildWithKeymaps(keymaps: FormKeymaps): FormSettings {
+    return new FormSettings(
+      keymaps,
+      this.search,
+      this.properties,
+      this.blacklist,
+    );
+  }
+
+  buildWithSearch(search: FormSearch): FormSettings {
+    return new FormSettings(
+      this.keymaps,
+      search,
+      this.properties,
+      this.blacklist,
+    );
+  }
+
+  buildWithProperties(props: settings.Properties): FormSettings {
+    return new FormSettings(
+      this.keymaps,
+      this.search,
+      props,
+      this.blacklist,
+    );
+  }
+
+  buildWithBlacklist(blacklist: string[]): FormSettings {
+    return new FormSettings(
+      this.keymaps,
+      this.search,
+      this.properties,
+      blacklist,
+    );
+  }
+
+  toSettings(): Settings {
+    return settings.valueOf({
+      keymaps: this.keymaps.toKeymaps(),
+      search: this.search.toSearchSettings(),
+      properties: this.properties,
+      blacklist: this.blacklist,
+    });
+  }
+
+  toJSON(): {
+    keymaps: ReturnType<FormKeymaps['toJSON']>;
+    search: ReturnType<FormSearch['toJSON']>;
+    properties: settings.Properties;
+    blacklist: string[];
+    } {
+    return {
+      keymaps: this.keymaps.toJSON(),
+      search: this.search.toJSON(),
+      properties: this.properties,
+      blacklist: this.blacklist,
+    };
+  }
+
+  static valueOf(o: ReturnType<FormSettings['toJSON']>): FormSettings {
+    for (let name of ['keymaps', 'search', 'properties', 'blacklist']) {
+      if (!Object.prototype.hasOwnProperty.call(o, name)) {
+        throw new Error(`"${name}" field not set`);
+      }
+    }
+    return new FormSettings(
+      FormKeymaps.valueOf(o.keymaps),
+      FormSearch.valueOf(o.search),
+      settings.propertiesValueOf(o.properties),
+      settings.blacklistValueOf(o.blacklist),
+    );
+  }
+
+  static fromSettings(data: Settings): FormSettings {
+    return new FormSettings(
+      FormKeymaps.fromKeymaps(data.keymaps),
+      FormSearch.fromSearch(data.search),
+      data.properties,
+      data.blacklist);
+  }
+}
+
+export enum SettingSource {
+  JSON = 'json',
+  Form = 'form',
+}
+
+export default class SettingData {
+  private source: SettingSource;
+
+  private json?: JSONSettings;
+
+  private form?: FormSettings;
+
+  constructor({
+    source, json, form
+  }: {
+    source: SettingSource,
+    json?: JSONSettings,
+    form?: FormSettings,
+  }) {
+    this.source = source;
+    this.json = json;
+    this.form = form;
+  }
+
+  getSource(): SettingSource {
+    return this.source;
+  }
+
+  getJSON(): JSONSettings {
+    if (!this.json) {
+      throw new TypeError('json settings not set');
+    }
+    return this.json;
+  }
+
+  getForm(): FormSettings {
+    if (!this.form) {
+      throw new TypeError('form settings not set');
+    }
+    return this.form;
+  }
+
+  toJSON(): any {
+    switch (this.source) {
+    case SettingSource.JSON:
+      return {
+        source: this.source,
+        json: (this.json as JSONSettings).toJSON(),
+      };
+    case SettingSource.Form:
+      return {
+        source: this.source,
+        form: (this.form as FormSettings).toJSON(),
+      };
+    }
+    throw new Error(`unknown settings source: ${this.source}`);
+  }
+
+  toSettings(): Settings {
+    switch (this.source) {
+    case SettingSource.JSON:
+      return this.getJSON().toSettings();
+    case SettingSource.Form:
+      return this.getForm().toSettings();
+    }
+    throw new Error(`unknown settings source: ${this.source}`);
+  }
+
+  static valueOf(o: {
+    source: string;
+    json?: string;
+    form?: ReturnType<FormSettings['toJSON']>;
+  }): SettingData {
+    switch (o.source) {
+    case SettingSource.JSON:
+      return new SettingData({
+        source: o.source,
+        json: JSONSettings.valueOf(
+          o.json as ReturnType<JSONSettings['toJSON']>),
+      });
+    case SettingSource.Form:
+      return new SettingData({
+        source: o.source,
+        form: FormSettings.valueOf(
+          o.form as ReturnType<FormSettings['toJSON']>),
+      });
+    }
+    throw new Error(`unknown settings source: ${o.source}`);
+  }
+}
+
+export const DefaultSettingData: SettingData = SettingData.valueOf({
+  source: 'json',
+  json: `{
+  "keymaps": {
+    "0": { "type": "scroll.home" },
+    ":": { "type": "command.show" },
+    "o": { "type": "command.show.open", "alter": false },
+    "O": { "type": "command.show.open", "alter": true },
+    "t": { "type": "command.show.tabopen", "alter": false },
+    "T": { "type": "command.show.tabopen", "alter": true },
+    "w": { "type": "command.show.winopen", "alter": false },
+    "W": { "type": "command.show.winopen", "alter": true },
+    "b": { "type": "command.show.buffer" },
+    "a": { "type": "command.show.addbookmark", "alter": true },
+    "k": { "type": "scroll.vertically", "count": -1 },
+    "j": { "type": "scroll.vertically", "count": 1 },
+    "h": { "type": "scroll.horizonally", "count": -1 },
+    "l": { "type": "scroll.horizonally", "count": 1 },
+    "<C-U>": { "type": "scroll.pages", "count": -0.5 },
+    "<C-D>": { "type": "scroll.pages", "count": 0.5 },
+    "<C-B>": { "type": "scroll.pages", "count": -1 },
+    "<C-F>": { "type": "scroll.pages", "count": 1 },
+    "gg": { "type": "scroll.top" },
+    "G": { "type": "scroll.bottom" },
+    "$": { "type": "scroll.end" },
+    "d": { "type": "tabs.close" },
+    "D": { "type": "tabs.close.right" },
+    "!d": { "type": "tabs.close.force" },
+    "u": { "type": "tabs.reopen" },
+    "K": { "type": "tabs.prev" },
+    "J": { "type": "tabs.next" },
+    "gT": { "type": "tabs.prev" },
+    "gt": { "type": "tabs.next" },
+    "g0": { "type": "tabs.first" },
+    "g$": { "type": "tabs.last" },
+    "<C-6>": { "type": "tabs.prevsel" },
+    "r": { "type": "tabs.reload", "cache": false },
+    "R": { "type": "tabs.reload", "cache": true },
+    "zp": { "type": "tabs.pin.toggle" },
+    "zd": { "type": "tabs.duplicate" },
+    "zi": { "type": "zoom.in" },
+    "zo": { "type": "zoom.out" },
+    "zz": { "type": "zoom.neutral" },
+    "f": { "type": "follow.start", "newTab": false },
+    "F": { "type": "follow.start", "newTab": true, "background": false },
+    "m": { "type": "mark.set.prefix" },
+    "'": { "type": "mark.jump.prefix" },
+    "H": { "type": "navigate.history.prev" },
+    "L": { "type": "navigate.history.next" },
+    "[[": { "type": "navigate.link.prev" },
+    "]]": { "type": "navigate.link.next" },
+    "gu": { "type": "navigate.parent" },
+    "gU": { "type": "navigate.root" },
+    "gi": { "type": "focus.input" },
+    "gf": { "type": "page.source" },
+    "gh": { "type": "page.home" },
+    "gH": { "type": "page.home", "newTab": true },
+    "y": { "type": "urls.yank" },
+    "p": { "type": "urls.paste", "newTab": false },
+    "P": { "type": "urls.paste", "newTab": true },
+    "/": { "type": "find.start" },
+    "n": { "type": "find.next" },
+    "N": { "type": "find.prev" },
+    "<S-Esc>": { "type": "addon.toggle.enabled" }
+  },
+  "search": {
+    "default": "google",
+    "engines": {
+      "google": "https://google.com/search?q={}",
+      "yahoo": "https://search.yahoo.com/search?p={}",
+      "bing": "https://www.bing.com/search?q={}",
+      "duckduckgo": "https://duckduckgo.com/?q={}",
+      "twitter": "https://twitter.com/search?q={}",
+      "wikipedia": "https://en.wikipedia.org/w/index.php?search={}"
+    }
+  },
+  "properties": {
+    "hintchars": "abcdefghijklmnopqrstuvwxyz",
+    "smoothscroll": false,
+    "complete": "sbh"
+  },
+  "blacklist": [
+  ]
+}`,
+});
diff --git a/src/shared/Settings.ts b/src/shared/Settings.ts
new file mode 100644
index 0000000..ce6b9ee
--- /dev/null
+++ b/src/shared/Settings.ts
@@ -0,0 +1,200 @@
+import * as operations from './operations';
+import * as PropertyDefs from './property-defs';
+
+export type Keymaps = {[key: string]: operations.Operation};
+
+export interface Search {
+  default: string;
+  engines: { [key: string]: string };
+}
+
+export interface Properties {
+  hintchars: string;
+  smoothscroll: boolean;
+  complete: string;
+}
+
+export default interface Settings {
+  keymaps: Keymaps;
+  search: Search;
+  properties: Properties;
+  blacklist: string[];
+  // eslint-disable-next-line semi
+}
+
+const DefaultProperties: Properties = PropertyDefs.defs.reduce(
+  (o: {[name: string]: PropertyDefs.Type}, def) => {
+    o[def.name] = def.defaultValue;
+    return o;
+  }, {}) as Properties;
+
+
+export const keymapsValueOf = (o: any): Keymaps => {
+  return Object.keys(o).reduce((keymaps: Keymaps, key: string): Keymaps => {
+    let op = operations.valueOf(o[key]);
+    keymaps[key] = op;
+    return keymaps;
+  }, {});
+};
+
+export const searchValueOf = (o: any): Search => {
+  if (typeof o.default !== 'string') {
+    throw new TypeError('string field "default" not set"');
+  }
+  for (let name of Object.keys(o.engines)) {
+    if ((/\s/).test(name)) {
+      throw new TypeError(
+        `While space in the search engine not allowed: "${name}"`);
+    }
+    let url = o.engines[name];
+    if (typeof url !== 'string') {
+      throw new TypeError('"engines" not an object of string');
+    }
+    let matches = url.match(/{}/g);
+    if (matches === null) {
+      throw new TypeError(`No {}-placeholders in URL of "${name}"`);
+    } else if (matches.length > 1) {
+      throw new TypeError(`Multiple {}-placeholders in URL of "${name}"`);
+    }
+
+  }
+  if (!Object.prototype.hasOwnProperty.call(o.engines, o.default)) {
+    throw new TypeError(`Default engine "${o.default}" not found`);
+  }
+  return {
+    default: o.default as string,
+    engines: { ...o.engines },
+  };
+};
+
+export const propertiesValueOf = (o: any): Properties => {
+  let defNames = new Set(PropertyDefs.defs.map(def => def.name));
+  let unknownName = Object.keys(o).find(name => !defNames.has(name));
+  if (unknownName) {
+    throw new TypeError(`Unknown property name: "${unknownName}"`);
+  }
+
+  for (let def of PropertyDefs.defs) {
+    if (!Object.prototype.hasOwnProperty.call(o, def.name)) {
+      continue;
+    }
+    if (typeof o[def.name] !== def.type) {
+      throw new TypeError(`property "${def.name}" is not ${def.type}`);
+    }
+  }
+  return {
+    ...DefaultProperties,
+    ...o,
+  };
+};
+
+export const blacklistValueOf = (o: any): string[] => {
+  if (!Array.isArray(o)) {
+    throw new TypeError(`"blacklist" is not an array of string`);
+  }
+  for (let x of o) {
+    if (typeof x !== 'string') {
+      throw new TypeError(`"blacklist" is not an array of string`);
+    }
+  }
+  return o as string[];
+};
+
+export const valueOf = (o: any): Settings => {
+  let settings = { ...DefaultSetting };
+  if (Object.prototype.hasOwnProperty.call(o, 'keymaps')) {
+    settings.keymaps = keymapsValueOf(o.keymaps);
+  }
+  if (Object.prototype.hasOwnProperty.call(o, 'search')) {
+    settings.search = searchValueOf(o.search);
+  }
+  if (Object.prototype.hasOwnProperty.call(o, 'properties')) {
+    settings.properties = propertiesValueOf(o.properties);
+  }
+  if (Object.prototype.hasOwnProperty.call(o, 'blacklist')) {
+    settings.blacklist = blacklistValueOf(o.blacklist);
+  }
+  return settings;
+};
+
+const DefaultSetting: Settings = {
+  keymaps: {
+    '0': { 'type': 'scroll.home' },
+    ':': { 'type': 'command.show' },
+    'o': { 'type': 'command.show.open', 'alter': false },
+    'O': { 'type': 'command.show.open', 'alter': true },
+    't': { 'type': 'command.show.tabopen', 'alter': false },
+    'T': { 'type': 'command.show.tabopen', 'alter': true },
+    'w': { 'type': 'command.show.winopen', 'alter': false },
+    'W': { 'type': 'command.show.winopen', 'alter': true },
+    'b': { 'type': 'command.show.buffer' },
+    'a': { 'type': 'command.show.addbookmark', 'alter': true },
+    'k': { 'type': 'scroll.vertically', 'count': -1 },
+    'j': { 'type': 'scroll.vertically', 'count': 1 },
+    'h': { 'type': 'scroll.horizonally', 'count': -1 },
+    'l': { 'type': 'scroll.horizonally', 'count': 1 },
+    '<C-U>': { 'type': 'scroll.pages', 'count': -0.5 },
+    '<C-D>': { 'type': 'scroll.pages', 'count': 0.5 },
+    '<C-B>': { 'type': 'scroll.pages', 'count': -1 },
+    '<C-F>': { 'type': 'scroll.pages', 'count': 1 },
+    'gg': { 'type': 'scroll.top' },
+    'G': { 'type': 'scroll.bottom' },
+    '$': { 'type': 'scroll.end' },
+    'd': { 'type': 'tabs.close' },
+    'D': { 'type': 'tabs.close.right' },
+    '!d': { 'type': 'tabs.close.force' },
+    'u': { 'type': 'tabs.reopen' },
+    'K': { 'type': 'tabs.prev' },
+    'J': { 'type': 'tabs.next' },
+    'gT': { 'type': 'tabs.prev' },
+    'gt': { 'type': 'tabs.next' },
+    'g0': { 'type': 'tabs.first' },
+    'g$': { 'type': 'tabs.last' },
+    '<C-6>': { 'type': 'tabs.prevsel' },
+    'r': { 'type': 'tabs.reload', 'cache': false },
+    'R': { 'type': 'tabs.reload', 'cache': true },
+    'zp': { 'type': 'tabs.pin.toggle' },
+    'zd': { 'type': 'tabs.duplicate' },
+    'zi': { 'type': 'zoom.in' },
+    'zo': { 'type': 'zoom.out' },
+    'zz': { 'type': 'zoom.neutral' },
+    'f': { 'type': 'follow.start', 'newTab': false, 'background': false },
+    'F': { 'type': 'follow.start', 'newTab': true, 'background': false },
+    'm': { 'type': 'mark.set.prefix' },
+    '\'': { 'type': 'mark.jump.prefix' },
+    'H': { 'type': 'navigate.history.prev' },
+    'L': { 'type': 'navigate.history.next' },
+    '[[': { 'type': 'navigate.link.prev' },
+    ']]': { 'type': 'navigate.link.next' },
+    'gu': { 'type': 'navigate.parent' },
+    'gU': { 'type': 'navigate.root' },
+    'gi': { 'type': 'focus.input' },
+    'gf': { 'type': 'page.source' },
+    'gh': { 'type': 'page.home', 'newTab': false },
+    'gH': { 'type': 'page.home', 'newTab': true },
+    'y': { 'type': 'urls.yank' },
+    'p': { 'type': 'urls.paste', 'newTab': false },
+    'P': { 'type': 'urls.paste', 'newTab': true },
+    '/': { 'type': 'find.start' },
+    'n': { 'type': 'find.next' },
+    'N': { 'type': 'find.prev' },
+    '<S-Esc>': { 'type': 'addon.toggle.enabled' }
+  },
+  search: {
+    default: 'google',
+    engines: {
+      'google': 'https://google.com/search?q={}',
+      'yahoo': 'https://search.yahoo.com/search?p={}',
+      'bing': 'https://www.bing.com/search?q={}',
+      'duckduckgo': 'https://duckduckgo.com/?q={}',
+      'twitter': 'https://twitter.com/search?q={}',
+      'wikipedia': 'https://en.wikipedia.org/w/index.php?search={}'
+    }
+  },
+  properties: {
+    hintchars: 'abcdefghijklmnopqrstuvwxyz',
+    smoothscroll: false,
+    complete: 'sbh'
+  },
+  blacklist: []
+};
diff --git a/src/shared/operations.ts b/src/shared/operations.ts
index cc22f75..688c240 100644
--- a/src/shared/operations.ts
+++ b/src/shared/operations.ts
@@ -443,5 +443,5 @@ export const valueOf = (o: any): Operation => {
   case MARK_JUMP_PREFIX:
     return { type: o.type };
   }
-  throw new Error('unknown operation type: ' + o.type);
+  throw new TypeError('unknown operation type: ' + o.type);
 };
diff --git a/src/shared/properties.ts b/src/shared/properties.ts
new file mode 100644
index 0000000..6315030
--- /dev/null
+++ b/src/shared/properties.ts
@@ -0,0 +1,50 @@
+export type Type = string | number | boolean;
+
+export class Def {
+  private name0: string;
+
+  private description0: string;
+
+  private defaultValue0: Type;
+
+  constructor(
+    name: string,
+    description: string,
+    defaultValue: Type,
+  ) {
+    this.name0 = name;
+    this.description0 = description;
+    this.defaultValue0 = defaultValue;
+  }
+
+  public get name(): string {
+    return this.name0;
+  }
+
+  public get defaultValue(): Type {
+    return this.defaultValue0;
+  }
+
+  public get description(): Type {
+    return this.description0;
+  }
+
+  public get type(): string {
+    return typeof this.defaultValue;
+  }
+}
+
+export const defs: Def[] = [
+  new Def(
+    'hintchars',
+    'hint characters on follow mode',
+    'abcdefghijklmnopqrstuvwxyz'),
+  new Def(
+    'smoothscroll',
+    'smooth scroll',
+    false),
+  new Def(
+    'complete',
+    'which are completed at the open page',
+    'sbh'),
+];
diff --git a/src/shared/property-defs.ts b/src/shared/property-defs.ts
new file mode 100644
index 0000000..6315030
--- /dev/null
+++ b/src/shared/property-defs.ts
@@ -0,0 +1,50 @@
+export type Type = string | number | boolean;
+
+export class Def {
+  private name0: string;
+
+  private description0: string;
+
+  private defaultValue0: Type;
+
+  constructor(
+    name: string,
+    description: string,
+    defaultValue: Type,
+  ) {
+    this.name0 = name;
+    this.description0 = description;
+    this.defaultValue0 = defaultValue;
+  }
+
+  public get name(): string {
+    return this.name0;
+  }
+
+  public get defaultValue(): Type {
+    return this.defaultValue0;
+  }
+
+  public get description(): Type {
+    return this.description0;
+  }
+
+  public get type(): string {
+    return typeof this.defaultValue;
+  }
+}
+
+export const defs: Def[] = [
+  new Def(
+    'hintchars',
+    'hint characters on follow mode',
+    'abcdefghijklmnopqrstuvwxyz'),
+  new Def(
+    'smoothscroll',
+    'smooth scroll',
+    false),
+  new Def(
+    'complete',
+    'which are completed at the open page',
+    'sbh'),
+];
diff --git a/src/shared/settings/default.ts b/src/shared/settings/default.ts
deleted file mode 100644
index 6523a74..0000000
--- a/src/shared/settings/default.ts
+++ /dev/null
@@ -1,85 +0,0 @@
-export default {
-  source: 'json',
-  json: `{
-  "keymaps": {
-    "0": { "type": "scroll.home" },
-    ":": { "type": "command.show" },
-    "o": { "type": "command.show.open", "alter": false },
-    "O": { "type": "command.show.open", "alter": true },
-    "t": { "type": "command.show.tabopen", "alter": false },
-    "T": { "type": "command.show.tabopen", "alter": true },
-    "w": { "type": "command.show.winopen", "alter": false },
-    "W": { "type": "command.show.winopen", "alter": true },
-    "b": { "type": "command.show.buffer" },
-    "a": { "type": "command.show.addbookmark", "alter": true },
-    "k": { "type": "scroll.vertically", "count": -1 },
-    "j": { "type": "scroll.vertically", "count": 1 },
-    "h": { "type": "scroll.horizonally", "count": -1 },
-    "l": { "type": "scroll.horizonally", "count": 1 },
-    "<C-U>": { "type": "scroll.pages", "count": -0.5 },
-    "<C-D>": { "type": "scroll.pages", "count": 0.5 },
-    "<C-B>": { "type": "scroll.pages", "count": -1 },
-    "<C-F>": { "type": "scroll.pages", "count": 1 },
-    "gg": { "type": "scroll.top" },
-    "G": { "type": "scroll.bottom" },
-    "$": { "type": "scroll.end" },
-    "d": { "type": "tabs.close" },
-    "D": { "type": "tabs.close.right" },
-    "!d": { "type": "tabs.close.force" },
-    "u": { "type": "tabs.reopen" },
-    "K": { "type": "tabs.prev", "count": 1 },
-    "J": { "type": "tabs.next", "count": 1 },
-    "gT": { "type": "tabs.prev", "count": 1 },
-    "gt": { "type": "tabs.next", "count": 1 },
-    "g0": { "type": "tabs.first" },
-    "g$": { "type": "tabs.last" },
-    "<C-6>": { "type": "tabs.prevsel" },
-    "r": { "type": "tabs.reload", "cache": false },
-    "R": { "type": "tabs.reload", "cache": true },
-    "zp": { "type": "tabs.pin.toggle" },
-    "zd": { "type": "tabs.duplicate" },
-    "zi": { "type": "zoom.in" },
-    "zo": { "type": "zoom.out" },
-    "zz": { "type": "zoom.neutral" },
-    "f": { "type": "follow.start", "newTab": false },
-    "F": { "type": "follow.start", "newTab": true, "background": false },
-    "m": { "type": "mark.set.prefix" },
-    "'": { "type": "mark.jump.prefix" },
-    "H": { "type": "navigate.history.prev" },
-    "L": { "type": "navigate.history.next" },
-    "[[": { "type": "navigate.link.prev" },
-    "]]": { "type": "navigate.link.next" },
-    "gu": { "type": "navigate.parent" },
-    "gU": { "type": "navigate.root" },
-    "gi": { "type": "focus.input" },
-    "gf": { "type": "page.source" },
-    "gh": { "type": "page.home" },
-    "gH": { "type": "page.home", "newTab": true },
-    "y": { "type": "urls.yank" },
-    "p": { "type": "urls.paste", "newTab": false },
-    "P": { "type": "urls.paste", "newTab": true },
-    "/": { "type": "find.start" },
-    "n": { "type": "find.next" },
-    "N": { "type": "find.prev" },
-    "<S-Esc>": { "type": "addon.toggle.enabled" }
-  },
-  "search": {
-    "default": "google",
-    "engines": {
-      "google": "https://google.com/search?q={}",
-      "yahoo": "https://search.yahoo.com/search?p={}",
-      "bing": "https://www.bing.com/search?q={}",
-      "duckduckgo": "https://duckduckgo.com/?q={}",
-      "twitter": "https://twitter.com/search?q={}",
-      "wikipedia": "https://en.wikipedia.org/w/index.php?search={}"
-    }
-  },
-  "properties": {
-    "hintchars": "abcdefghijklmnopqrstuvwxyz",
-    "smoothscroll": false,
-    "complete": "sbh"
-  },
-  "blacklist": [
-  ]
-}`,
-};
diff --git a/src/shared/settings/properties.ts b/src/shared/settings/properties.ts
deleted file mode 100644
index 7d037df..0000000
--- a/src/shared/settings/properties.ts
+++ /dev/null
@@ -1,24 +0,0 @@
-// describe types of a propety as:
-//    mystr: 'string',
-//    mynum: 'number',
-//    mybool: 'boolean',
-const types: { [key: string]: string } = {
-  hintchars: 'string',
-  smoothscroll: 'boolean',
-  complete: 'string',
-};
-
-// describe default values of a property
-const defaults: { [key: string]: string | number | boolean } = {
-  hintchars: 'abcdefghijklmnopqrstuvwxyz',
-  smoothscroll: false,
-  complete: 'sbh',
-};
-
-const docs: { [key: string]: string } = {
-  hintchars: 'hint characters on follow mode',
-  smoothscroll: 'smooth scroll',
-  complete: 'which are completed at the open page',
-};
-
-export { types, defaults, docs };
diff --git a/src/shared/settings/storage.ts b/src/shared/settings/storage.ts
deleted file mode 100644
index 90a3a66..0000000
--- a/src/shared/settings/storage.ts
+++ /dev/null
@@ -1,32 +0,0 @@
-import DefaultSettings from './default';
-import * as settingsValues from './values';
-
-const loadRaw = async(): Promise<any> => {
-  let { settings } = await browser.storage.local.get('settings');
-  if (!settings) {
-    return DefaultSettings;
-  }
-  return { ...DefaultSettings, ...settings as object };
-};
-
-const loadValue = async() => {
-  let settings = await loadRaw();
-  let value = JSON.parse(DefaultSettings.json);
-  if (settings.source === 'json') {
-    value = settingsValues.valueFromJson(settings.json);
-  } else if (settings.source === 'form') {
-    value = settingsValues.valueFromForm(settings.form);
-  }
-  if (!value.properties) {
-    value.properties = {};
-  }
-  return { ...settingsValues.valueFromJson(DefaultSettings.json), ...value };
-};
-
-const save = (settings: any): Promise<any> => {
-  return browser.storage.local.set({
-    settings,
-  });
-};
-
-export { loadRaw, loadValue, save };
diff --git a/src/shared/settings/validator.ts b/src/shared/settings/validator.ts
deleted file mode 100644
index 71cc466..0000000
--- a/src/shared/settings/validator.ts
+++ /dev/null
@@ -1,76 +0,0 @@
-import * as operations from '../operations';
-import * as properties from './properties';
-
-const VALID_TOP_KEYS = ['keymaps', 'search', 'blacklist', 'properties'];
-const VALID_OPERATION_VALUES = Object.keys(operations).map((key) => {
-  return operations[key];
-});
-
-const validateInvalidTopKeys = (settings: any): void => {
-  let invalidKey = Object.keys(settings).find((key) => {
-    return !VALID_TOP_KEYS.includes(key);
-  });
-  if (invalidKey) {
-    throw Error(`Unknown key: "${invalidKey}"`);
-  }
-};
-
-const validateKeymaps = (keymaps: any): void => {
-  for (let key of Object.keys(keymaps)) {
-    let value = keymaps[key];
-    if (!VALID_OPERATION_VALUES.includes(value.type)) {
-      throw Error(`Unknown operation: "${value.type}"`);
-    }
-  }
-};
-
-const validateSearch = (search: any): void => {
-  let engines = search.engines;
-  for (let key of Object.keys(engines)) {
-    if ((/\s/).test(key)) {
-      throw new Error(
-        `While space in search engine name is not allowed: "${key}"`
-      );
-    }
-    let url = engines[key];
-    if (!url.match(/{}/)) {
-      throw new Error(`No {}-placeholders in URL of "${key}"`);
-    }
-    if (url.match(/{}/g).length > 1) {
-      throw new Error(`Multiple {}-placeholders in URL of "${key}"`);
-    }
-  }
-
-  if (!search.default) {
-    throw new Error(`Default engine is not set`);
-  }
-  if (!Object.keys(engines).includes(search.default)) {
-    throw new Error(`Default engine "${search.default}" not found`);
-  }
-};
-
-const validateProperties = (props: any): void => {
-  for (let name of Object.keys(props)) {
-    if (!properties.types[name]) {
-      throw new Error(`Unknown property name: "${name}"`);
-    }
-    if (typeof props[name] !== properties.types[name]) {
-      throw new Error(`Invalid type for property: "${name}"`);
-    }
-  }
-};
-
-const validate = (settings: any): void => {
-  validateInvalidTopKeys(settings);
-  if (settings.keymaps) {
-    validateKeymaps(settings.keymaps);
-  }
-  if (settings.search) {
-    validateSearch(settings.search);
-  }
-  if (settings.properties) {
-    validateProperties(settings.properties);
-  }
-};
-
-export { validate };
diff --git a/src/shared/settings/values.ts b/src/shared/settings/values.ts
deleted file mode 100644
index cb6a668..0000000
--- a/src/shared/settings/values.ts
+++ /dev/null
@@ -1,108 +0,0 @@
-import * as properties from './properties';
-
-const operationFromFormName = (name: string): any => {
-  let [type, argStr] = name.split('?');
-  let args = {};
-  if (argStr) {
-    args = JSON.parse(argStr);
-  }
-  return { type, ...args };
-};
-
-const operationToFormName = (op: any): string => {
-  let type = op.type;
-  let args = { ...op };
-  delete args.type;
-
-  if (Object.keys(args).length === 0) {
-    return type;
-  }
-  return op.type + '?' + JSON.stringify(args);
-};
-
-const valueFromJson = (json: string): object => {
-  return JSON.parse(json);
-};
-
-const valueFromForm = (form: any): object => {
-  let keymaps: any = undefined;
-  if (form.keymaps) {
-    keymaps = {};
-    for (let name of Object.keys(form.keymaps)) {
-      let keys = form.keymaps[name];
-      keymaps[keys] = operationFromFormName(name);
-    }
-  }
-
-  let search: any = undefined;
-  if (form.search) {
-    search = { default: form.search.default };
-
-    if (form.search.engines) {
-      search.engines = {};
-      for (let [name, url] of form.search.engines) {
-        search.engines[name] = url;
-      }
-    }
-  }
-
-  return {
-    keymaps,
-    search,
-    blacklist: form.blacklist,
-    properties: form.properties
-  };
-};
-
-const jsonFromValue = (value: any): string => {
-  return JSON.stringify(value, undefined, 2);
-};
-
-const formFromValue = (value: any, allowedOps: any[]): any => {
-  let keymaps: any = undefined;
-
-  if (value.keymaps) {
-    let allowedSet = new Set(allowedOps);
-
-    keymaps = {};
-    for (let keys of Object.keys(value.keymaps)) {
-      let op = operationToFormName(value.keymaps[keys]);
-      if (allowedSet.has(op)) {
-        keymaps[op] = keys;
-      }
-    }
-  }
-
-  let search: any = undefined;
-  if (value.search) {
-    search = { default: value.search.default };
-    if (value.search.engines) {
-      search.engines = Object.keys(value.search.engines).map((name) => {
-        return [name, value.search.engines[name]];
-      });
-    }
-  }
-
-  let formProperties = { ...properties.defaults, ...value.properties };
-
-  return {
-    keymaps,
-    search,
-    blacklist: value.blacklist,
-    properties: formProperties,
-  };
-};
-
-const jsonFromForm = (form: any): string => {
-  return jsonFromValue(valueFromForm(form));
-};
-
-const formFromJson = (json: string, allowedOps: any[]): any => {
-  let value = valueFromJson(json);
-  return formFromValue(value, allowedOps);
-};
-
-export {
-  valueFromJson, valueFromForm, jsonFromValue, formFromValue,
-  jsonFromForm, formFromJson
-};
diff --git a/test/background/usecases/parsers.test.ts b/test/background/usecases/parsers.test.ts
index 17b034b..f3a64eb 100644
--- a/test/background/usecases/parsers.test.ts
+++ b/test/background/usecases/parsers.test.ts
@@ -3,45 +3,32 @@ import * as parsers from 'background/usecases/parsers';
 describe("shared/commands/parsers", () => {
   describe("#parsers.parseSetOption", () => {
     it('parse set string', () => {
-      let [key, value] = parsers.parseSetOption('encoding=utf-8', { encoding: 'string' });
-      expect(key).to.equal('encoding');
-      expect(value).to.equal('utf-8');
+      let [key, value] = parsers.parseSetOption('hintchars=abcdefgh');
+      expect(key).to.equal('hintchars');
+      expect(value).to.equal('abcdefgh');
     });
 
     it('parse set empty string', () => {
-      let [key, value] = parsers.parseSetOption('encoding=', { encoding: 'string' });
-      expect(key).to.equal('encoding');
+      let [key, value] = parsers.parseSetOption('hintchars=');
+      expect(key).to.equal('hintchars');
       expect(value).to.equal('');
     });
 
-    it('parse set string', () => {
-      let [key, value] = parsers.parseSetOption('history=50', { history: 'number' });
-      expect(key).to.equal('history');
-      expect(value).to.equal(50);
-    });
-
     it('parse set boolean', () => {
-      let [key, value] = parsers.parseSetOption('paste', { paste: 'boolean' });
-      expect(key).to.equal('paste');
+      let [key, value] = parsers.parseSetOption('smoothscroll');
+      expect(key).to.equal('smoothscroll');
       expect(value).to.be.true;
 
-      [key, value] = parsers.parseSetOption('nopaste', { paste: 'boolean' });
-      expect(key).to.equal('paste');
+      [key, value] = parsers.parseSetOption('nosmoothscroll');
+      expect(key).to.equal('smoothscroll');
       expect(value).to.be.false;
     });
 
     it('throws error on unknown property', () => {
-      expect(() => parsers.parseSetOption('charset=utf-8', {})).to.throw(Error, 'Unknown');
-      expect(() => parsers.parseSetOption('smoothscroll', {})).to.throw(Error, 'Unknown');
-      expect(() => parsers.parseSetOption('nosmoothscroll', {})).to.throw(Error, 'Unknown');
-    })
-
-    it('throws error on invalid property', () => {
-      expect(() => parsers.parseSetOption('charset=utf-8', { charset: 'number' })).to.throw(Error, 'Not number');
-      expect(() => parsers.parseSetOption('charset=utf-8', { charset: 'boolean' })).to.throw(Error, 'Invalid');
-      expect(() => parsers.parseSetOption('charset=', { charset: 'boolean' })).to.throw(Error, 'Invalid');
-      expect(() => parsers.parseSetOption('smoothscroll', { smoothscroll: 'string' })).to.throw(Error, 'Invalid');
-      expect(() => parsers.parseSetOption('smoothscroll', { smoothscroll: 'number' })).to.throw(Error, 'Invalid');
-    })
+      expect(() => parsers.parseSetOption('encoding=utf-8')).to.throw(Error, 'Unknown');
+      expect(() => parsers.parseSetOption('paste')).to.throw(Error, 'Unknown');
+      expect(() => parsers.parseSetOption('nopaste')).to.throw(Error, 'Unknown');
+      expect(() => parsers.parseSetOption('smoothscroll=yes')).to.throw(Error, 'Invalid argument');
+    });
   });
 });
diff --git a/test/content/actions/setting.test.ts b/test/content/actions/setting.test.ts
index 0721d5d..c831433 100644
--- a/test/content/actions/setting.test.ts
+++ b/test/content/actions/setting.test.ts
@@ -4,32 +4,40 @@ import * as settingActions from 'content/actions/setting';
 describe("setting actions", () => {
   describe("set", () => {
     it('create SETTING_SET action', () => {
-      let action = settingActions.set({ red: 'apple', yellow: 'banana' });
+      let action = settingActions.set({
+        keymaps: {
+          'dd': 'remove current tab',
+          'z<C-A>': 'increment',
+        },
+        search: {
+          default: "google",
+          engines: {
+            google: 'https://google.com/search?q={}',
+          }
+        },
+        properties: {
+          hintchars: 'abcd1234',
+        },
+        blacklist: [],
+      });
       expect(action.type).to.equal(actions.SETTING_SET);
-      expect(action.value.red).to.equal('apple');
-      expect(action.value.yellow).to.equal('banana');
-      expect(action.value.keymaps).to.be.empty;
+      expect(action.settings.properties.hintchars).to.equal('abcd1234');
     });
 
-    it('converts keymaps', () => {
+    it('overrides cancel keys', () => {
       let action = settingActions.set({
         keymaps: {
-          'dd': 'remove current tab',
-          'z<C-A>': 'increment',
+          "k": { "type": "scroll.vertically", "count": -1 },
+          "j": { "type": "scroll.vertically", "count": 1 },
         }
       });
-      let keymaps = action.value.keymaps;
-      let map = new Map(keymaps);
-      expect(map).to.have.deep.all.keys(
-        [
-          [{ key: 'Esc', shiftKey: false, ctrlKey: false, altKey: false, metaKey: false }],
-          [{ key: '[', shiftKey: false, ctrlKey: true, altKey: false, metaKey: false }],
-          [{ key: 'd', shiftKey: false, ctrlKey: false, altKey: false, metaKey: false },
-           { key: 'd', shiftKey: false, ctrlKey: false, altKey: false, metaKey: false }],
-          [{ key: 'z', shiftKey: false, ctrlKey: false, altKey: false, metaKey: false },
-           { key: 'a', shiftKey: false, ctrlKey: true, altKey: false, metaKey: false }],
-        ]
-      );
+      let keymaps = action.settings.keymaps;
+      expect(action.settings.keymaps).to.deep.equals({
+        "k": { type: "scroll.vertically", count: -1 },
+        "j": { type: "scroll.vertically", count: 1 },
+        '<Esc>': { type: 'cancel' },
+        '<C-[>': { type: 'cancel' },
+      });
     });
   });
 });
diff --git a/test/content/reducers/setting.test.ts b/test/content/reducers/setting.test.ts
index 226fc58..fe23006 100644
--- a/test/content/reducers/setting.test.ts
+++ b/test/content/reducers/setting.test.ts
@@ -9,9 +9,24 @@ describe("content setting reducer", () => {
 
   it('return next state for SETTING_SET', () => {
     let newSettings = { red: 'apple', yellow: 'banana' };
-    let action = { type: actions.SETTING_SET, value: newSettings };
+    let action = {
+      type: actions.SETTING_SET,
+      settings: {
+        keymaps: {
+          "zz": { type: "zoom.neutral" },
+          "<S-Esc>": { "type": "addon.toggle.enabled" }
+        },
+        "blacklist": []
+      }
+    }
     let state = settingReducer(undefined, action);
-    expect(state).to.deep.equal(newSettings);
-    expect(state).not.to.equal(newSettings);  // assert deep copy
+    console.log(JSON.stringify(state.keymaps));
+    expect(state.keymaps).to.have.deep.all.members([
+      { key: [{ key: 'z', shiftKey: false, ctrlKey: false, altKey: false, metaKey: false },
+              { key: 'z', shiftKey: false, ctrlKey: false, altKey: false, metaKey: false }],
+        op: { type: 'zoom.neutral' }},
+      { key: [{ key: 'Esc', shiftKey: true, ctrlKey: false, altKey: false, metaKey: false }],
+        op: { type: 'addon.toggle.enabled' }},
+    ]);
   });
 });
diff --git a/test/settings/components/form/KeymapsForm.test.tsx b/test/settings/components/form/KeymapsForm.test.tsx
index 6ac57c9..dc2322b 100644
--- a/test/settings/components/form/KeymapsForm.test.tsx
+++ b/test/settings/components/form/KeymapsForm.test.tsx
@@ -2,15 +2,17 @@ import React from 'react';
 import ReactDOM from 'react-dom';
 import ReactTestRenderer from 'react-test-renderer';
 import ReactTestUtils from 'react-dom/test-utils';
-import KeymapsForm from 'settings/components/form/KeymapsForm'
+import KeymapsForm from '../../../../src/settings/components/form/KeymapsForm'
+import { FormKeymaps } from 'shared/SettingData';
+import { expect } from 'chai';
 
 describe("settings/form/KeymapsForm", () => {
   describe('render', () => {
     it('renders keymap fields', () => {
-      let root = ReactTestRenderer.create(<KeymapsForm value={{
+      let root = ReactTestRenderer.create(<KeymapsForm value={FormKeymaps.valueOf({
         'scroll.vertically?{"count":1}': 'j',
         'scroll.vertically?{"count":-1}': 'k',
-      }} />).root
+      })} />).root
 
       let inputj = root.findByProps({ id: 'scroll.vertically?{"count":1}' });
       let inputk = root.findByProps({ id: 'scroll.vertically?{"count":-1}' });
@@ -46,12 +48,12 @@ describe("settings/form/KeymapsForm", () => {
     it('invokes onChange event on edit', (done) => {
       ReactTestUtils.act(() => {
         ReactDOM.render(<KeymapsForm
-          value={{
+          value={FormKeymaps.valueOf({
             'scroll.vertically?{"count":1}': 'j',
             'scroll.vertically?{"count":-1}': 'k',
-          }}
+          })}
           onChange={value => {
-            expect(value['scroll.vertically?{"count":1}']).to.equal('jjj');
+            expect(value.toJSON()['scroll.vertically?{"count":1}']).to.equal('jjj');
             done();
           }} />, container);
       });
diff --git a/test/settings/components/form/SearchEngineForm.test.tsx b/test/settings/components/form/SearchEngineForm.test.tsx
index 06822f2..0e6b17d 100644
--- a/test/settings/components/form/SearchEngineForm.test.tsx
+++ b/test/settings/components/form/SearchEngineForm.test.tsx
@@ -3,14 +3,15 @@ import ReactDOM from 'react-dom';
 import ReactTestRenderer from 'react-test-renderer';
 import ReactTestUtils from 'react-dom/test-utils';
 import SearchForm from 'settings/components/form/SearchForm'
+import { FormSearch } from 'shared/SettingData';
 
 describe("settings/form/SearchForm", () => {
   describe('render', () => {
     it('renders SearchForm', () => {
-      let root = ReactTestRenderer.create(<SearchForm value={{
+      let root = ReactTestRenderer.create(<SearchForm value={FormSearch.valueOf({
         default: 'google',
         engines: [['google', 'google.com'], ['yahoo', 'yahoo.com']],
-      }} />).root;
+      })} />).root;
 
       let names = root.findAllByProps({ name: 'name' });
       expect(names).to.have.lengthOf(2);
@@ -22,28 +23,6 @@ describe("settings/form/SearchForm", () => {
       expect(urls[0].props.value).to.equal('google.com');
       expect(urls[1].props.value).to.equal('yahoo.com');
     });
-
-    it('renders blank value', () => {
-      let root = ReactTestRenderer.create(<SearchForm />).root;
-
-      let names = root.findAllByProps({ name: 'name' });
-      expect(names).to.be.empty;
-
-      let urls = root.findAllByProps({ name: 'url' });
-      expect(urls).to.be.empty;
-    });
-
-    it('renders blank engines', () => {
-      let root = ReactTestRenderer.create(
-        <SearchForm value={{ default: 'google' }} />,
-      ).root;
-
-      let names = root.findAllByProps({ name: 'name' });
-      expect(names).to.be.empty;
-
-      let urls = root.findAllByProps({ name: 'url' });
-      expect(urls).to.be.empty;
-    });
   });
 
   describe('onChange event', () => {
@@ -62,14 +41,15 @@ describe("settings/form/SearchForm", () => {
     it('invokes onChange event on edit', (done) => {
       ReactTestUtils.act(() => {
         ReactDOM.render(<SearchForm
-          value={{
+          value={FormSearch.valueOf({
             default: 'google',
               engines: [['google', 'google.com'], ['yahoo', 'yahoo.com']]
-          }}
+          })}
           onChange={value => {
-            expect(value.default).to.equal('louvre');
-            expect(value.engines).to.have.lengthOf(2)
-            expect(value.engines).to.have.deep.members(
+            let json = value.toJSON();
+            expect(json.default).to.equal('louvre');
+            expect(json.engines).to.have.lengthOf(2)
+            expect(json.engines).to.have.deep.members(
               [['louvre', 'google.com'], ['yahoo', 'yahoo.com']]
             );
             done();
@@ -87,14 +67,15 @@ describe("settings/form/SearchForm", () => {
 
     it('invokes onChange event on delete', (done) => {
       ReactTestUtils.act(() => {
-        ReactDOM.render(<SearchForm value={{
+        ReactDOM.render(<SearchForm value={FormSearch.valueOf({
             default: 'yahoo',
             engines: [['louvre', 'google.com'], ['yahoo', 'yahoo.com']]
-          }}
+          })}
           onChange={value => {
-            expect(value.default).to.equal('yahoo');
-            expect(value.engines).to.have.lengthOf(1)
-            expect(value.engines).to.have.deep.members(
+            let json = value.toJSON();
+            expect(json.default).to.equal('yahoo');
+            expect(json.engines).to.have.lengthOf(1)
+            expect(json.engines).to.have.deep.members(
               [['yahoo', 'yahoo.com']]
             );
             done();
@@ -107,14 +88,15 @@ describe("settings/form/SearchForm", () => {
 
     it('invokes onChange event on add', (done) => {
       ReactTestUtils.act(() => {
-        ReactDOM.render(<SearchForm value={{
+        ReactDOM.render(<SearchForm value={FormSearch.valueOf({
           default: 'yahoo',
             engines: [['google', 'google.com']]
-          }}
+          })}
           onChange={value => {
-            expect(value.default).to.equal('yahoo');
-            expect(value.engines).to.have.lengthOf(2)
-            expect(value.engines).to.have.deep.members(
+            let json = value.toJSON();
+            expect(json.default).to.equal('yahoo');
+            expect(json.engines).to.have.lengthOf(2)
+            expect(json.engines).to.have.deep.members(
               [['google', 'google.com'], ['', '']],
             );
             done();
diff --git a/test/settings/reducers/setting.test.ts b/test/settings/reducers/setting.test.ts
index 6a874e8..376d66e 100644
--- a/test/settings/reducers/setting.test.ts
+++ b/test/settings/reducers/setting.test.ts
@@ -4,8 +4,7 @@ import settingReducer from 'settings/reducers/setting';
 describe("settings setting reducer", () => {
   it('return the initial state', () => {
     let state = settingReducer(undefined, {});
-    expect(state).to.have.deep.property('json', '');
-    expect(state).to.have.deep.property('form', null);
+    expect(state).to.have.deep.property('source', 'json');
     expect(state).to.have.deep.property('error', '');
   });
 
diff --git a/test/shared/SettingData.test.ts b/test/shared/SettingData.test.ts
new file mode 100644
index 0000000..8736ecb
--- /dev/null
+++ b/test/shared/SettingData.test.ts
@@ -0,0 +1,293 @@
+import SettingData, {
+  FormKeymaps, JSONSettings, FormSettings,
+} from '../../src/shared/SettingData';
+import Settings, { Keymaps } from '../../src/shared/Settings';
+import { expect } from 'chai';
+
+describe('shared/SettingData', () => {
+  describe('FormKeymaps', () => {
+    describe('#valueOF to #toKeymaps', () => {
+      it('parses form keymaps and convert to operations', () => {
+        let data = {
+          'scroll.vertically?{"count":1}': 'j',
+          'scroll.home': '0',
+        }
+
+        let keymaps = FormKeymaps.valueOf(data).toKeymaps();
+        expect(keymaps).to.deep.equal({
+          'j': { type: 'scroll.vertically', count: 1 },
+          '0': { type: 'scroll.home' },
+        });
+      });
+    });
+
+    describe('#fromKeymaps to #toJSON', () => {
+      it('create from a Keymaps and create a JSON object', () => {
+        let data: Keymaps = {
+          'j': { type: 'scroll.vertically', count: 1 },
+          '0': { type: 'scroll.home' },
+        }
+
+        let keymaps = FormKeymaps.fromKeymaps(data).toJSON();
+        expect(keymaps).to.deep.equal({
+          'scroll.vertically?{"count":1}': 'j',
+          'scroll.home': '0',
+        });
+      });
+    });
+  });
+
+  describe('JSONSettings', () => {
+    describe('#valueOf to #toSettings', () => {
+      it('parse object and create a Settings', () => {
+        let o = `{
+          "keymaps": {},
+          "search": {
+            "default": "google",
+            "engines": {
+              "google": "https://google.com/search?q={}"
+            }
+          },
+          "properties": {
+            "hintchars": "abcdefghijklmnopqrstuvwxyz",
+            "smoothscroll": false,
+            "complete": "sbh"
+          },
+          "blacklist": []
+        }`;
+
+        let settings = JSONSettings.valueOf(o).toSettings();
+        expect(settings).to.deep.equal(JSON.parse(o));
+      });
+    });
+
+    describe('#fromSettings to #toJSON', () => {
+      it('create from a Settings and create a JSON string', () => {
+        let o = {
+          keymaps: {},
+          search: {
+            default: "google",
+            engines: {
+              google: "https://google.com/search?q={}",
+            },
+          },
+          properties: {
+            hintchars: "abcdefghijklmnopqrstuvwxyz",
+            smoothscroll: false,
+            complete: "sbh"
+          },
+          blacklist: [],
+        };
+
+        let json = JSONSettings.fromSettings(o).toJSON();
+        expect(JSON.parse(json)).to.deep.equal(o);
+      });
+    });
+  });
+
+  describe('FormSettings', () => {
+    describe('#valueOf to #toSettings', () => {
+      it('parse object and create a Settings', () => {
+        let data = {
+          keymaps: {
+            'scroll.vertically?{"count":1}': 'j',
+            'scroll.home': '0',
+          },
+          search: {
+            default: "google",
+            engines: [
+              ["google", "https://google.com/search?q={}"],
+            ]
+          },
+          properties: {
+            hintchars: "abcdefghijklmnopqrstuvwxyz",
+            smoothscroll: false,
+            complete: "sbh"
+          },
+          blacklist: []
+        };
+
+        let settings = FormSettings.valueOf(data).toSettings();
+        expect(settings).to.deep.equal({
+          keymaps: {
+            'j': { type: 'scroll.vertically', count: 1 },
+            '0': { type: 'scroll.home' },
+          },
+          search: {
+            default: "google",
+            engines: {
+              "google": "https://google.com/search?q={}"
+            }
+          },
+          properties: {
+            hintchars: "abcdefghijklmnopqrstuvwxyz",
+            smoothscroll: false,
+            complete: "sbh"
+          },
+          blacklist: []
+        });
+      });
+    });
+
+    describe('#fromSettings to #toJSON', () => {
+      it('create from a Settings and create a JSON string', () => {
+        let data: Settings = {
+          keymaps: {
+            'j': { type: 'scroll.vertically', count: 1 },
+            '0': { type: 'scroll.home' },
+          },
+          search: {
+            default: "google",
+            engines: {
+              "google": "https://google.com/search?q={}"
+            }
+          },
+          properties: {
+            hintchars: "abcdefghijklmnopqrstuvwxyz",
+            smoothscroll: false,
+            complete: "sbh"
+          },
+          blacklist: []
+        };
+
+        let json = FormSettings.fromSettings(data).toJSON();
+        expect(json).to.deep.equal({
+          keymaps: {
+            'scroll.vertically?{"count":1}': 'j',
+            'scroll.home': '0',
+          },
+          search: {
+            default: "google",
+            engines: [
+              ["google", "https://google.com/search?q={}"],
+            ]
+          },
+          properties: {
+            hintchars: "abcdefghijklmnopqrstuvwxyz",
+            smoothscroll: false,
+            complete: "sbh"
+          },
+          blacklist: [],
+        });
+      });
+    });
+  });
+
+  describe('SettingData', () => {
+    describe('#valueOf to #toJSON', () => {
+      it('parse object from json source', () => {
+        let data = {
+          source: 'json',
+          json: `{
+            "keymaps": {},
+            "search": {
+              "default": "google",
+              "engines": {
+                "google": "https://google.com/search?q={}"
+              }
+            },
+            "properties": {
+              "hintchars": "abcdefghijklmnopqrstuvwxyz",
+              "smoothscroll": false,
+              "complete": "sbh"
+            },
+            "blacklist": []
+          }`,
+        };
+
+        let j = SettingData.valueOf(data).toJSON();
+        expect(j.source).to.equal('json');
+        expect(j.json).to.be.a('string');
+      });
+
+      it('parse object from form source', () => {
+        let data = {
+          source: 'form',
+          form: {
+            keymaps: {},
+            search: {
+              default: "yahoo",
+              engines: [
+                ['yahoo', 'https://yahoo.com/search?q={}'],
+              ],
+            },
+            properties: {
+              hintchars: "abcdefghijklmnopqrstuvwxyz",
+              smoothscroll: false,
+              complete: "sbh"
+            },
+            blacklist: [],
+          },
+        };
+
+        let j = SettingData.valueOf(data).toJSON();
+        expect(j.source).to.equal('form');
+        expect(j.form).to.deep.equal({
+          keymaps: {},
+          search: {
+            default: "yahoo",
+            engines: [
+              ['yahoo', 'https://yahoo.com/search?q={}'],
+            ],
+          },
+          properties: {
+            hintchars: "abcdefghijklmnopqrstuvwxyz",
+            smoothscroll: false,
+            complete: "sbh"
+          },
+          blacklist: [],
+        });
+      });
+    });
+
+    describe('#toSettings', () => {
+      it('parse object from json source', () => {
+        let data = {
+          source: 'json',
+          json: `{
+            "keymaps": {},
+            "search": {
+              "default": "google",
+              "engines": {
+                "google": "https://google.com/search?q={}"
+              }
+            },
+            "properties": {
+              "hintchars": "abcdefghijklmnopqrstuvwxyz",
+              "smoothscroll": false,
+              "complete": "sbh"
+            },
+            "blacklist": []
+          }`,
+        };
+
+        let settings = SettingData.valueOf(data).toSettings();
+        expect(settings.search.default).to.equal('google');
+      });
+
+      it('parse object from form source', () => {
+        let data = {
+          source: 'form',
+          form: {
+            keymaps: {},
+            search: {
+              default: "yahoo",
+              engines: [
+                ['yahoo', 'https://yahoo.com/search?q={}'],
+              ],
+            },
+            properties: {
+              hintchars: "abcdefghijklmnopqrstuvwxyz",
+              smoothscroll: false,
+              complete: "sbh"
+            },
+            blacklist: [],
+          },
+        };
+
+        let settings = SettingData.valueOf(data).toSettings();
+        expect(settings.search.default).to.equal('yahoo');
+      });
+    });
+  });
+});
diff --git a/test/shared/Settings.test.ts b/test/shared/Settings.test.ts
new file mode 100644
index 0000000..02cd022
--- /dev/null
+++ b/test/shared/Settings.test.ts
@@ -0,0 +1,190 @@
+import * as settings from '../../src/shared/Settings';
+import { expect } from 'chai';
+
+describe('Settings', () => {
+  describe('#keymapsValueOf', () => {
+    it('returns empty object by empty settings', () => {
+      let keymaps = settings.keymapsValueOf({});
+      expect(keymaps).to.be.empty;
+    });
+
+    it('returns keymaps by valid settings', () => {
+      let keymaps = settings.keymapsValueOf({
+        k: { type: "scroll.vertically", count: -1 },
+        j: { type: "scroll.vertically", count: 1 },
+      });
+
+      expect(keymaps['k']).to.deep.equal({ type: "scroll.vertically", count: -1 });
+      expect(keymaps['j']).to.deep.equal({ type: "scroll.vertically", count: 1 });
+    });
+
+    it('throws a TypeError by invalid settings', () => {
+      expect(() => settings.keymapsValueOf(null)).to.throw(TypeError);
+      expect(() => settings.keymapsValueOf({
+        k: { type: "invalid.operation" },
+      })).to.throw(TypeError);
+    });
+  });
+
+  describe('#searchValueOf', () => {
+    it('returns search settings by valid settings', () => {
+      let search = settings.searchValueOf({
+        default: "google",
+        engines: {
+          "google": "https://google.com/search?q={}",
+          "yahoo": "https://search.yahoo.com/search?p={}",
+        }
+      });
+
+      expect(search).to.deep.equal({
+        default: "google",
+        engines: {
+          "google": "https://google.com/search?q={}",
+          "yahoo": "https://search.yahoo.com/search?p={}",
+        }
+      });
+    });
+
+    it('throws a TypeError by invalid settings', () => {
+      expect(() => settings.searchValueOf(null)).to.throw(TypeError);
+      expect(() => settings.searchValueOf({})).to.throw(TypeError);
+      expect(() => settings.searchValueOf([])).to.throw(TypeError);
+      expect(() => settings.searchValueOf({
+        default: 123,
+        engines: {}
+      })).to.throw(TypeError);
+      expect(() => settings.searchValueOf({
+        default: "google",
+        engines: {
+          "google": 123456,
+        }
+      })).to.throw(TypeError);
+      expect(() => settings.searchValueOf({
+        default: "wikipedia",
+        engines: {
+          "google": "https://google.com/search?q={}",
+          "yahoo": "https://search.yahoo.com/search?p={}",
+        }
+      })).to.throw(TypeError);
+      expect(() => settings.searchValueOf({
+        default: "g o o g l e",
+        engines: {
+          "g o o g l e": "https://google.com/search?q={}",
+        }
+      })).to.throw(TypeError);
+      expect(() => settings.searchValueOf({
+        default: "google",
+        engines: {
+          "google": "https://google.com/search",
+        }
+      })).to.throw(TypeError);
+      expect(() => settings.searchValueOf({
+        default: "google",
+        engines: {
+          "google": "https://google.com/search?q={}&r={}",
+        }
+      })).to.throw(TypeError);
+    });
+  });
+
+  describe('#propertiesValueOf', () => {
+    it('returns with default properties by empty settings', () => {
+      let props = settings.propertiesValueOf({});
+      expect(props).to.deep.equal({
+        hintchars: "abcdefghijklmnopqrstuvwxyz",
+        smoothscroll: false,
+        complete: "sbh"
+      })
+    });
+
+    it('returns properties by valid settings', () => {
+      let props = settings.propertiesValueOf({
+        hintchars: "abcdefgh",
+        smoothscroll: false,
+        complete: "sbh"
+      });
+
+      expect(props).to.deep.equal({
+        hintchars: "abcdefgh",
+        smoothscroll: false,
+        complete: "sbh"
+      });
+    });
+
+    it('throws a TypeError by invalid settings', () => {
+      expect(() => settings.keymapsValueOf(null)).to.throw(TypeError);
+      expect(() => settings.keymapsValueOf({
+        smoothscroll: 'false',
+      })).to.throw(TypeError);
+      expect(() => settings.keymapsValueOf({
+        unknown: 'xyz'
+      })).to.throw(TypeError);
+    });
+  });
+
+  describe('#blacklistValueOf', () => {
+    it('returns empty array by empty settings', () => {
+      let blacklist = settings.blacklistValueOf([]);
+      expect(blacklist).to.be.empty;
+    });
+
+    it('returns blacklist by valid settings', () => {
+      let blacklist = settings.blacklistValueOf([
+        "github.com",
+        "circleci.com",
+      ]);
+
+      expect(blacklist).to.deep.equal([
+        "github.com",
+        "circleci.com",
+      ]);
+    });
+
+    it('throws a TypeError by invalid settings', () => {
+      expect(() => settings.blacklistValueOf(null)).to.throw(TypeError);
+      expect(() => settings.blacklistValueOf({})).to.throw(TypeError);
+      expect(() => settings.blacklistValueOf([1,2,3])).to.throw(TypeError);
+    });
+  });
+
+  describe('#valueOf', () => {
+    it('returns settings by valid settings', () => {
+      let x = settings.valueOf({
+        keymaps: {},
+        "search": {
+          "default": "google",
+          "engines": {
+            "google": "https://google.com/search?q={}",
+          }
+        },
+        "properties": {},
+        "blacklist": []
+      });
+
+      expect(x).to.deep.equal({
+        keymaps: {},
+        search: {
+          default: "google",
+          engines: {
+            google: "https://google.com/search?q={}",
+          }
+        },
+        properties: {
+          hintchars: "abcdefghijklmnopqrstuvwxyz",
+          smoothscroll: false,
+          complete: "sbh"
+        },
+        blacklist: []
+      });
+    });
+
+    it('sets default settings', () => {
+      let value = settings.valueOf({});
+      expect(value.keymaps).to.not.be.empty;
+      expect(value.properties).to.not.be.empty;
+      expect(value.search.default).to.be.a('string');
+      expect(value.search.engines).to.be.an('object');
+      expect(value.blacklist).to.be.empty;
+    });
+  });
+});
diff --git a/test/shared/properties.test.js b/test/shared/properties.test.js
new file mode 100644
index 0000000..37903d8
--- /dev/null
+++ b/test/shared/properties.test.js
@@ -0,0 +1,18 @@
+import * as settings from 'shared/settings';
+
+describe('properties', () => {
+  describe('Def class', () => {
+    it('returns property definitions', () => {
+      let def = new proerties.Def(
+        'smoothscroll',
+        'smooth scroll',
+        false);
+
+      expect(def.name).to.equal('smoothscroll');
+      expect(def.describe).to.equal('smooth scroll');
+      expect(def.defaultValue).to.equal(false);
+      expect(def.type).to.equal('boolean');
+    });
+  });
+});
+
diff --git a/test/shared/property-defs.test.js b/test/shared/property-defs.test.js
new file mode 100644
index 0000000..37903d8
--- /dev/null
+++ b/test/shared/property-defs.test.js
@@ -0,0 +1,18 @@
+import * as settings from 'shared/settings';
+
+describe('properties', () => {
+  describe('Def class', () => {
+    it('returns property definitions', () => {
+      let def = new proerties.Def(
+        'smoothscroll',
+        'smooth scroll',
+        false);
+
+      expect(def.name).to.equal('smoothscroll');
+      expect(def.describe).to.equal('smooth scroll');
+      expect(def.defaultValue).to.equal(false);
+      expect(def.type).to.equal('boolean');
+    });
+  });
+});
+
diff --git a/test/shared/settings/validator.test.ts b/test/shared/settings/validator.test.ts
deleted file mode 100644
index 9bbfa3e..0000000
--- a/test/shared/settings/validator.test.ts
+++ /dev/null
@@ -1,81 +0,0 @@
-import { validate } from 'shared/settings/validator';
-
-describe("setting validator", () => {
-  describe("unknown top keys", () => {
-    it('throws an error for unknown settings', () => {
-      let settings = { keymaps: {}, poison: 123 };
-      let fn = validate.bind(undefined, settings)
-      expect(fn).to.throw(Error, 'poison');
-    })
-  });
-
-  describe("keymaps settings", () => {
-    it('throws an error for unknown operation', () => {
-      let settings = {
-        keymaps: {
-          a: { 'type': 'scroll.home' },
-          b: { 'type': 'poison.dressing' },
-        }
-      };
-      let fn = validate.bind(undefined, settings)
-      expect(fn).to.throw(Error, 'poison.dressing');
-    });
-  });
-
-  describe("search settings", () => {
-    it('throws an error for invalid search engine name', () => {
-      let settings = {
-        search: {
-          default: 'google',
-          engines: {
-            'google': 'https://google.com/search?q={}',
-            'cherry pie': 'https://cherypie.com/search?q={}',
-          }
-        }
-      };
-      let fn = validate.bind(undefined, settings)
-      expect(fn).to.throw(Error, 'cherry pie');
-    });
-
-    it('throws an error for no {}-placeholder', () => {
-      let settings = {
-        search: {
-          default: 'google',
-          engines: {
-            'google': 'https://google.com/search?q={}',
-            'yahoo': 'https://search.yahoo.com/search',
-          }
-        }
-      };
-      let fn = validate.bind(undefined, settings)
-      expect(fn).to.throw(Error, 'yahoo');
-    });
-
-    it('throws an error for no default engines', () => {
-      let settings = {
-        search: {
-          engines: {
-            'google': 'https://google.com/search?q={}',
-            'yahoo': 'https://search.yahoo.com/search?q={}',
-          }
-        }
-      };
-      let fn = validate.bind(undefined, settings)
-      expect(fn).to.throw(Error, 'Default engine');
-    });
-
-    it('throws an error for invalid default engine', () => {
-      let settings = {
-        search: {
-          default: 'twitter',
-          engines: {
-            'google': 'https://google.com/search?q={}',
-            'yahoo': 'https://search.yahoo.com/search?q={}',
-          }
-        }
-      };
-      let fn = validate.bind(undefined, settings)
-      expect(fn).to.throw(Error, 'twitter');
-    });
-  });
-});
diff --git a/test/shared/settings/values.test.ts b/test/shared/settings/values.test.ts
deleted file mode 100644
index c72824d..0000000
--- a/test/shared/settings/values.test.ts
+++ /dev/null
@@ -1,138 +0,0 @@
-import * as values from 'shared/settings/values';
-
-describe("settings values", () => {
-  describe('valueFromJson', () => {
-    it('return object from json string', () => {
-      let json = `{
-        "keymaps": { "0": {"type": "scroll.home"}},
-        "search": { "default": "google", "engines": { "google": "https://google.com/search?q={}" }},
-        "blacklist": [ "*.slack.com"],
-        "properties": {
-          "mystr": "value",
-          "mynum": 123,
-          "mybool": true
-        }
-      }`;
-      let value = values.valueFromJson(json);
-
-      expect(value.keymaps).to.deep.equal({ 0: {type: "scroll.home"}});
-      expect(value.search).to.deep.equal({ default: "google", engines: { google: "https://google.com/search?q={}"} });
-      expect(value.blacklist).to.deep.equal(["*.slack.com"]);
-      expect(value.properties).to.have.property('mystr', 'value');
-      expect(value.properties).to.have.property('mynum', 123);
-      expect(value.properties).to.have.property('mybool', true);
-    });
-  });
-
-  describe('valueFromForm', () => {
-    it('returns value from form', () => {
-      let form = {
-        keymaps: {
-          'scroll.vertically?{"count":1}': 'j',
-          'scroll.home': '0',
-        },
-        search: {
-          default: 'google',
-          engines: [['google', 'https://google.com/search?q={}']],
-        },
-        blacklist: ['*.slack.com'],
-        "properties": {
-          "mystr": "value",
-          "mynum": 123,
-          "mybool": true,
-        }
-      };
-      let value = values.valueFromForm(form);
-
-      expect(value.keymaps).to.have.deep.property('j', { type: "scroll.vertically", count: 1 });
-      expect(value.keymaps).to.have.deep.property('0', { type: "scroll.home" });
-      expect(JSON.stringify(value.search)).to.deep.equal(JSON.stringify({ default: "google", engines: { google: "https://google.com/search?q={}"} }));
-      expect(value.search).to.deep.equal({ default: "google", engines: { google: "https://google.com/search?q={}"} });
-      expect(value.blacklist).to.deep.equal(["*.slack.com"]);
-      expect(value.properties).to.have.property('mystr', 'value');
-      expect(value.properties).to.have.property('mynum', 123);
-      expect(value.properties).to.have.property('mybool', true);
-    });
-
-    it('convert from empty form', () => {
-      let form = {};
-      let value = values.valueFromForm(form);
-      expect(value).to.not.have.key('keymaps');
-      expect(value).to.not.have.key('search');
-      expect(value).to.not.have.key('blacklist');
-      expect(value).to.not.have.key('properties');
-    });
-
-    it('override keymaps', () => {
-      let form = {
-        keymaps: {
-          'scroll.vertically?{"count":1}': 'j',
-          'scroll.vertically?{"count":-1}': 'j',
-        }
-      };
-      let value = values.valueFromForm(form);
-
-      expect(value.keymaps).to.have.key('j');
-    });
-
-    it('override search engine', () => {
-      let form = {
-        search: {
-          default: 'google',
-          engines: [
-            ['google', 'https://google.com/search?q={}'],
-            ['google', 'https://google.co.jp/search?q={}'],
-          ]
-        }
-      };
-      let value = values.valueFromForm(form);
-
-      expect(value.search.engines).to.have.property('google', 'https://google.co.jp/search?q={}');
-    });
-  });
-
-  describe('jsonFromValue', () => {
-  });
-
-  describe('formFromValue', () => {
-    it('convert empty value to form', () => {
-      let value = {};
-      let form = values.formFromValue(value);
-
-      expect(value).to.not.have.key('keymaps');
-      expect(value).to.not.have.key('search');
-      expect(value).to.not.have.key('blacklist');
-    });
-
-    it('convert value to form', () => {
-      let value = {
-        keymaps: {
-          j: { type: 'scroll.vertically', count: 1 },
-          JJ: { type: 'scroll.vertically', count: 100 },
-          0: { type: 'scroll.home' },
-        },
-        search: { default: 'google', engines: { google: 'https://google.com/search?q={}' }},
-        blacklist: [ '*.slack.com'],
-        properties: {
-          "mystr": "value",
-          "mynum": 123,
-          "mybool": true,
-        }
-      };
-      let allowed = ['scroll.vertically?{"count":1}', 'scroll.home' ];
-      let form = values.formFromValue(value, allowed);
-
-      expect(form.keymaps).to.have.property('scroll.vertically?{"count":1}', 'j');
-      expect(form.keymaps).to.not.have.property('scroll.vertically?{"count":100}');
-      expect(form.keymaps).to.have.property('scroll.home', '0');
-      expect(Object.keys(form.keymaps)).to.have.lengthOf(2);
-      expect(form.search).to.have.property('default', 'google');
-      expect(form.search).to.have.deep.property('engines', [['google', 'https://google.com/search?q={}']]);
-      expect(form.blacklist).to.have.lengthOf(1);
-      expect(form.blacklist).to.include('*.slack.com');
-      expect(form.properties).to.have.property('mystr', 'value');
-      expect(form.properties).to.have.property('mynum', 123);
-      expect(form.properties).to.have.property('mybool', true);
-    });
-  });
-});
-- 
cgit v1.2.3