Using TypeScript With Knockout

 |  TypeScript, Knockout

Knockout is a minimalist, mature and proven library for creating web applications. It isn’t as feature rich as some of the more modern libraries & frameworks but it does what it does well, primarily being binding HTML elements against a data model. Even in 2019, I believe Knockout has its place and is still used on some very large projects, including Microsoft’s Azure Portal.

As of v3.5.0 (released Feb 2019), Knockout has built-in TypeScript definitions. https://github.com/knockout/knockout/blob/master/build/types/knockout.d.ts

TypeScript provides a huge amount of benefits over JavaScript including type safety, improved IDE tooling and additional constructs like interfaces and enums. If you have an existing project that uses Knockout, you can move over to TypeScript now and start enjoying these benefits.

This article will start with creating a new project that uses webpack and Babel to transpile TypeScript down to JavaScript (ES5). We’ll then cover using TypeScript and Knockout’s type definitions to create view models, components and bindings that are strongly typed.

The code for this article is available on the GitHub repo: https://github.com/JonUK/knockout-typescript


Creating a new Knockout project with TypeScript support

Fire up your terminal / command prompt and let’s get stuck in!

Create a new folder for our project.

mkdir knockout-typescript
cd knockout-typescript

From within the new folder, initialize a new package.json.

npm init -y

Now we have our newly created package.json we can install Knockout and TypeScript.

npm install --save knockout
npm install --save-dev typescript

TypeScript is installed with the --save-dev argument to save it as a development dependency as it’s not needed in production.

Our project is going to use webpack to:

  • Handle transpiling TypeScript to Javascript and bundling into a single file
  • Generate a HTML page that includes a reference to the output bundle
  • During development, provide a web server that supports live reloading

Let’s install webpack and the other related packages we’re going to use.

npm install --save-dev webpack webpack-cli webpack-dev-server html-webpack-plugin

We’ll use Babel (with webpack) to actually transpile our TypeScript to JavaScript which can then be run by web browsers. Let’s install Babel and other related packages.

npm install --save-dev @babel/core @babel/preset-env @babel/preset-typescript @babel/plugin-proposal-class-properties babel-loader

We’re going to use Knockout components with HTML templates and want these templates to be bundled up by webpack. The effect this will have is when the page loads, the component templates will be ready immediately without any additional calls to the server. We’ll use the webpack loader html-loader to include our HTML in the webpack bundle. npm install --save-dev html-loader

We now need to add the webpack configuration file webpack.config.js.

const path = require('path');  
const HtmlWebpackPlugin = require('html-webpack-plugin');  
  
module.exports = {  
  mode: 'development',  
  entry: {  
    app: './src/index.ts'  
  },  
  devtool: 'source-map', // Generate separate source map files  
  devServer: {  
    contentBase: './dist',  
    overlay: true // Show errors in overlay on the website  
  },  
  module: {  
    rules: [  
      {  
        test: /\.(js|ts)$/,  
        exclude: /node_modules/,  
        loader: 'babel-loader'  
      },  
      {  
        test: /\.html$/, // All Knockout.js component HTML templates  
        use: 'html-loader' // Adds the component templates to the bundle  
      }  
    ]  
  },  
  plugins: [  
    new HtmlWebpackPlugin({  
      template: './webpack-template/index.html'  
    }),  
  ],  
  resolve: {  
    extensions: ['.js', '.ts']  
  }  
};

This configuration file instructs webpack to use babel-loader to transpile TypeScript (and ES6+) files down to a JavaScript (ES5) bundle which is output to the dist folder. HTML files are also included in the bundle by html-loader. The app entry file src/index.ts and the webpack template HTML file webpack-template/index.html used to generate dist/index.html don’t exist yet so lets create these now.

Create the file src/index.ts.

alert('An alert from index.ts');

This is just some test code which will allow us to check everything is working correctly. Later we’ll add some real code here.

Create the file webpack-template/index.html.

<!DOCTYPE html>  
<html lang="en">  
<head>  
  <meta charset="utf-8">  
  <meta name="viewport" content="width=device-width, initial-scale=1" />  
  <title>Knockout TypeScript</title>  
  
  <link  
  rel="stylesheet"  
  href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css"  
  integrity="sha384-Gn5384xqQ1aoWXA+058RXPxPg6fy4IWvTNh0E263XmFcJlSAwiGgFAW/dAiS6JXm"  
  crossorigin="anonymous">  
  
</head>  
<body>  
  
<main class="container">  
  <h1 class="mt-4">Using TypeScript With Knockout</h1>  
  
  <!-- The element Knockout will bind to -->
  <div id="app">
  </div>
</main>  
  
</body>  
</html>

As well as the configuration file for webpack, we also need to add configuration files for Babel TypeScript.

Create the file Babel configuration file .babelrc.

{  
  "presets": [  
    "@babel/preset-env",  
    "@babel/typescript"  
  ],  
  "plugins": [  
    "@babel/proposal-class-properties"  
  ]  
}

Create the TypeScript configuration file tsconfig.json

{  
  "compilerOptions": {  
    /* Basic Options */  
    "target": "esnext", 
    "module": "commonjs",
    "allowJs": true, 
    "noEmit": true,
    "isolatedModules": true,
  
    /* Strict Type-Checking Options */
    "strict": true,
   
    /* Module Resolution Options */
    "moduleResolution": "node",
    "baseUrl": "./src",
  
    /* Advanced Options */ 
    "forceConsistentCasingInFileNames": true
  },  
  "include": ["src"]  
}

We’re almost ready to test our page out by running the webpack dev server. Just before we do that though, let’s update the scripts section in package.json to include a new entries for starting the webpack dev server and for performing a production build.

{
  ...
  "scripts": {  
    "serve": "webpack-dev-server --open",  
    "build": "webpack --mode production",  
  },
  ...
}

Good work! Now you can run npm run serve to start dev server and load the site. You should see the JavaScript alert that we added in index.ts.


Using strong types with Knockout

We’re now going to create a Person class that will be used by a read-only and an editable component to display & allow editing of a person’s details.

Create the file src/models/person.ts.

import * as ko from 'knockout';  
import {
  Observable, ObservableArray, PureComputed
} from 'knockout';  
  
class Person {  
  firstName: Observable<string>;  
  lastName: Observable<string>;  
  favouriteFilms: ObservableArray<string>;  
  
  fullName: PureComputed<string> = ko.pureComputed(
    () => this.firstName() + ' ' + this.lastName());  
  
  constructor(firstName: string, lastName: string, favouriteFilms: string[] | null) {  
    this.firstName = ko.observable(firstName);  
    this.lastName = ko.observable(lastName);  
    this.favouriteFilms = ko.observableArray(favouriteFilms || []);  
  }  
}  
  
export default Person;

The types Observable, ObservableArray and Purecomputed have all been pulled in from knockout. First name is an Observable of type string so if you try to set the firstName observable to a null, a boolean, a number etc, you’ll receive a TypeScript error. Similarly, the PureComputed must always return a string and favouriteFilms can only contain an array of string.

Let’s now create the read-only component to show the person’s details. Create the file src/components/PersonReadOnly.ts.

import Person from '../models/person';  
import template from './PersonReadOnly.html';  
  
class PersonReadOnly {  
  person: Person;  
  
  constructor(params: any) {  
    this.person = params.person;  
  }  
}  
  
// The default export returns the component details object to register with KO  
export default { viewModel: PersonReadOnly, template: template };

The template (which we’ll create next) is imported which will result in a HTML string being included in the webpack bundle.

Create the template src/components/PersonReadOnly.html for the read-only component.

<div>  
  
  <h2 class="h4 mt-4 mb-3">PersonReadOnly Component</h2>  
  
  <table class="table">  
    <thead>  
    <tr>  
      <th scope="col">Item</th>  
      <th scope="col">Value</th>  
    </tr>  
    </thead>  
    <tbody>  
    <tr>  
      <th scope="row">First name</th>  
      <td data-bind="text: person.firstName"></td>  
    </tr>  
    <tr>  
      <th scope="row">Last name</th>  
      <td data-bind="text: person.lastName"></td>  
    </tr>  
    <tr>  
      <th scope="row">Full name</th>  
      <td data-bind="text: person.fullName"></td>  
    </tr>  
    <tr>  
      <th scope="row">Favourite films</th>  
      <td data-bind="films: person.favouriteFilms"></td>  
    </tr>  
    </tbody>  
  </table>  
  
</div>

The first name observable, last name observable and full name computed are all bound using the built-in text binding. The favourite films observable array is however bound with a custom films binding which we’ll create next.

Create the file src/bindings/filmsBinding.ts.

import * as ko from 'knockout';  
import { BindingHandler } from 'knockout';  
  
const filmsBinding = {  
  init: (element: any, valueAccessor: () => any): void => {  
    const valueUnwrapped = ko.unwrap(valueAccessor());  
    const isPopulatedArray = Array.isArray(valueUnwrapped) &&
      valueUnwrapped.length > 0;  
    const text = isPopulatedArray ? valueUnwrapped.join(', ') : 'Unknown';  
  
    element.textContent = text;  
  }  
} as BindingHandler;  
  
export default filmsBinding;

Now we’ll create the editable component. Create the file src/components/PersonEditable.ts.

import Person from '../models/person';  
import template from './PersonEditable.html';  
  
class PersonReadOnly {  
  person: Person;  
  
  constructor(params: any) {  
    this.person = params.person;  
  }  
}  
  
// The default export returns the component details object to register with KO  
export default { viewModel: PersonReadOnly, template: template };

And create the editable component template src/components/PersonEditable.html.

<div>  
  
  <h2 class="h4 mt-4 mb-3">PersonEditable Component</h2>  
  
  <form>  
    <div class="form-group">  
      <label for="firstName">First name</label>  
      <input data-bind="textInput: person.firstName"
        type="text" id="firstName" class="form-control">  
    </div>  
  
    <div class="form-group">  
      <label for="lastName">Last name</label>  
      <input data-bind="textInput: person.lastName"
        type="text" id="lastName" class="form-control">  
    </div>  
  
  </form>  
  
</div>

We can now reference and use these components in index.ts.

import * as ko from 'knockout';  
  
import Person from "./models/person";  
import PersonReadOnly from './components/PersonReadOnly';  
import PersonEditable from './components/PersonEditable';  
  
import filmsBinding from './bindings/filmsBinding';  
  
ko.components.register('person-read-only', PersonReadOnly);  
ko.components.register('person-editable', PersonEditable);  
  
ko.bindingHandlers.films = filmsBinding;  
  
class AppViewModel {  
  person: Person;  
  
  constructor() {  
  
    // These values are hard-coded but could come from a server API request with JSON response  
  this.person = new Person(  
      'Jon',  
      'Keeping',  
      ['The Matrix', 'The Shawshank Redemption', 'Upgrade']  
    )  
  }  
}  
  
  
ko.applyBindings(
  new AppViewModel(),
  document.getElementById('app'));

Here we’re importing and then registering the components and the custom binding. We’re also creating a instance of the Person class on the person property of AppViewModel. All that’s left to do now is update the webpack template to use these 2 components.

Update the <div id="app"> element in webpack-template/index.html to contain:

<div id="app">  
  
  <div class="row">  
    <div class="col-sm-12 col-lg-7">  
      <person-read-only params="person: person"></person-read-only>  
    </div>  
    <div class="col-sm-12 col-lg-5 pl-lg-5">  
      <person-editable params="person: person"></person-editable>  
    </div>  
  </div>  
  
</div>

Now when you run npm run serve and reload the the browser you’ll see these strongly typed Knockout components in action.

The code for this article is available on the GitHub repo: https://github.com/JonUK/knockout-typescript