Library Pattern in JavaScript and TypeScript

Image of Author
September 16, 2024 (last updated September 18, 2024)

This is a pattern I have settled on over time as an effective way to create a library of functions in JavaScript and TypeScript projects that can be used universally across an application.

For prior art in techniques like this see Barrel Pattern in JavaScript and TypeScript

The goal of the library pattern is an autocompleting type-safe self-discoverable self-documenting on-hover lib namespace that contains arbitrarily many functions, consts, etc., organised within arbitrarily many namespaces.

import * as lib from '../../lib'

export function action() {
  const data = lib.namespace.subnamespace.func1()
  return lib.otherNamespace.func2(data)
}

(To keep things purely function in the code above, you could pass in the library as a parameter action(lib), or action({ lib }). The TS type can be typeof lib.)

This strategy implementation leverages the fact that JS gives special treatment to files named index.js. When you import a directory, e.g., ../lib, if that directory has an index.js file within it, what you actually import is lib/index.js. This means you can index the contents of you lib to control what information is made "public". You can do this recursively down the lib hierarchy, by which I mean all nested namespaces can have an index.js file as well, e.g., lib/namespace/index.js.

The power of indexing is in information hiding. You can have helper functions that are not ever exported from your nested files. You can also export a function from a file but not re-export it from the index.js file. This allows you to do things like test drive a function that you explicitly import into a test file, while also hiding it from the rest of the codebase which is only ever importing lib. In cases where you export a lot of files for testing but do not re-export them in index.js files, you do run the risk of accidentally explicitly importing the function instead of relying on your lib export. This means there is a soft requirement of team consensus to use the library pattern as intended.

Here are a few upsides to this approach:

  • You have to think less about where you put functionality. It always lives in lib! Are you worried you are about to write new code that's already been written? Just check lib! (You still have to design your namespaces, which likely evolve over time. But, manual refactoring is relatively easy in this system because searching and replacing lib.namespace.someFunc is guaranteed to hit.)
  • lib. will let you autocomplete your way to victory.
  • JSDoc will "just work", at least at the function/export level. Adding JSDoc to namespaces can be a bit tricker.
  • lib can end up being the "functional core" of your application. It is amenable to testing, refactoring, and composition.

Example

// src/lib/namespace/one.js

/** jsdoc for oneString */
export function oneString() {
  return 'one'
}

/** jsdoc for oneNumber */
export function oneNumber() {
  return 1
}
// src/lib/namespace/two.js

/** jsdoc for twoString */
export function twoString() {
  return 'two'
}

/** jsdoc for twoNumber */
export function twoNumber() {
  return 2
}
// src/lib/namespace/index.js

/**
- If you don't want to annotate namespace then you can
- just re-export
*/
export * as one from './one'
export * as two from './two'


/**
- If you want to annotate namespaces you have create
- space to do so
*/
import * as one from './one'
import * as two from './two'

export {
  /** jsdoc for namespace `one` */
  one,
  /** jsdoc for namespace `two` */
  two,
}

/** you can also define function here */
// ./lib/index.js

import * as namespace from './namespace'

export {
  /** jsdoc for namespace `namespace` */
  namespace
}

Now, when you import lib from anywhere in the codebase you will have an autocompleting type-safe self-discoverable self-documenting-on-hover lib!

// src/some/other/file.js

import * as lib from './lib'

function idk() {
  const one = lib.namespace.one.oneString()
}