knockout-contrib

:metal: KnockoutJS Goodies Monorepo

View the Project on GitHub Profiscience/knockout-contrib

TypeScript Support

If you’re using TypeScript — as you most definitely should be — you’re in luck! The router is written with first-class support for TypeScript.

That being said, it isn’t immediately obvious how to take advantage of type-safety if you use middleware and plugins that extend the context.

Let’s say you want to have some middleware that sets a data property on the context, like so…

import { Router } from '@profiscience/knockout-contrib'

Router.use(async (ctx) => {
  ctx.data = await fetchSomeData()
})

Because ctx is strongly-typed, we get an error as expected…

Property ‘data’ does not exist on type ‘Context & IContext’

We could use the dirty (ctx as any).data = ... hack that anyone who has used TypeScript for more than a day has undoubtedly had to use, but it would be way better if we could let the compiler know about the new property so we get all the benefits type-safety brings to the table like autocompletion. To do this, it’s as simple as adding a declare statement to our middleware file, like so…

import { Router } from '@profiscience/knockout-contrib'

declare module '@profiscience/knockout-contrib-router' {
  interface IContext {
    data?: MyDataType
  }
}

Router.use(async (ctx) => {
  ctx.data = await fetchSomeData()
})

That’s it! If fetchSomeData() returns something of the wrong type, the compiler will throw an error, and in your component viewModels, you will have full autocomplete of your custom properties.

NOTE: It’s an interface prefixed with I, not the normal Context class. This is because TypeScript does not support declaration merging on classes.

You may also take advantage of some types that are exported, namely RouteConfig, RouteMap, Middleware, and RoutePlugin. You can use these to specify types where the compiler cannot otherwise infer them. For example…

import { Plugin } from '@profiscience/knockout-contrib'

declare module '@profiscience/knockout-contrib-router' {
  interface IContext {
    data: string
  }
  interface IRouteConfig {
    apiUrl: string
  }
}

const apiPlugin: Plugin = ({ apiUrl }) => async (ctx) => {
  ctx.data = await $.get(apiUrl)
}

export default apiPlugin

Note, we also augment IRouteConfig so that we can get type-safety with the route constructor, i.e.

import { Route } from '@profiscience/knockout-contrib'

new Route('/', {
  // this is type-safe!
  apiUrl: 'https://example.com',
})