A Solid Approach To Documenting, Coding, Testing, and Publishing a Typescript Function
Write fully-tested and documented functions worth publishing in their own packages.
The Opinionated TLDR
The Opinionated Architect’s TLDR:
- To write a typescript function, start hacking and using the function where needed.
- Next, we are doing Behavior Driven Development, so comment out that code and any initial tests! Don't inject features into your code without testing. If we remove any code, the tests should fail.
- Write documentation in a README.md (or preferred documentation solution) using TSDoc (or JSDoc) and Markdown. Leverage a tool like Grammarly and ChatGPT while writing documentation.
- List out all the functional features which are used as test cases.
- Write the first test case and code focusing on the core feature. This test case will also become the example code in our documentation!
- Publish the code to a public or private software repository as a package. If it's worth writing, then it's worth publishing.
The code for parentPath
is available here.
Functions and Libraries
It isn’t uncommon for a node package to only expose a single function (is-number and is-plain-object being examples). This article focuses on publishing a single function: a sort of simplification of the process. We don’t mean to imply that the same process isn’t applied to publishing a library of functions or even a framework.
Introduction
I wanted to write a function that, given a path, it would return an array that included the path and all possible paths above it. I started by hacking something quickly and calling the functionpathHierarchy
. Naming, being one of the two most challenging things in software, I eventually figured the name wasn't that great.
Prerequisites
- Your project has a testing framework setup (we are using Jest).
- Your project has a way to manage versions and publish your function (we use lerna and npmjs.com).
- Your project has a place for documentation. Even something as simple as a README.md suffices.
The Approach
1) Hack For Understanding
Initially, hack out some solution with the intent to "throw it away." Use the function a few times in the code base to ensure it does what is needed. Be sloppy. Don't worry about names. Do focus on best-known practices, such as a function should only have one purpose.
2) Comment Out The Code
Why comment out all the code? Eventually, we want to apply Behavior Driven Development to create our function. We want to DRIVE the writing of the code through testing. If we keep around our existing code, we may leak untested behavior.
But the coverage shows 100% test coverage, and all code paths are verified!
Code coverage reports let us know if a developer used BDD to create the code: if the coverage isn't 100%, then we didn't use BDD. However, code coverage reports are terrible at assuring feature and behavior coverage (see example below).
If we remove any code, the tests should fail.
Note: If you have some initial tests, also comment those out too. We'll add them back in one by one assuring they fail before re-writing our code.
3a) Document the Description
Have a README.md, or another place for documentation, for your project.
Start writing the documentation, preferably in Markdown, for the function using TSDoc (or JSDoc). Why use Markdown? The popup help in a tool like Visual Studio Code uses Markdown.
My writing skills are atrocious, so I write documentation with something like Grammarly or ChatGPT constantly checking my work.
After some thought, the description for the function was:
Given an absolute path, it returns an array containing all possible parent paths, including the root path ordered by the child path to the root path.
The function description made me realize that the initial function name pathHierarchy
didn't express the function's intent. The new name parentPaths
felt better (but probably not that great).
Note: Automating the creation documentation from code would be preferable.
3b) Document the Function Signature
We already know the function signature because we had the hacked code to review. The final markdown documentation for the function was:
## **parentPaths** - Getting All Paths of a Child PathGiven an absolute path, `parentPaths` returns an array containing all possible parent paths, including the root path ordered by the child path to the root path.**@remarks**
The child path is normalized before all possible parent paths are generated. For example, `/cat/../and/mouse` becomes `/and/mouse`.* **@param childPath** - The absolute path used to generate all possible parent paths.
* **@throws** - An error is thrown if `childPath` is not an absolute path.
* **@returns** - An array containing all possible parent paths
including the root path ordered by the child path first.
We are going for Behavior Driven Development, and while we develop, we can leverage our documentation via popup help giving us a gut feel for its usefulness. So, try and get the documentation in the code soon.
4) List Out The Features
Using the documentation and hacked code, create a list of features of the function. For our function, we have:
- The function is given a path and returns an array of all possible parent paths.
- The array has a sort order from the child path to the root path.
- An absolute path is required, so we need to verify the path is absolute and error out if it isn't.
We now have an excellent start for our test: each one of those features becomes at least one test case.
5) Write The Tests and Write the Code
We can now write the code and the tests starting with the core feature.
import {
parentPaths,
} from '../src/index';describe('parentPaths', () => {
describe('POSIX', () => {
it(`should return all all possible parent paths
including the child path`, async () => {
const result: string[] = parentPaths('/cat/mouse');
expect(result).toEqual([
'/cat/mouse',
'/cat',
'/',
]);
});
});
});
While writing the first test, remember that we can use it as an example in our documentation! We get tested example code!
Make sure your test fails before writing the code. Comment/uncomment the code as needed if you aren't sure the test covers what you expect it to cover.
Repeat for all the features.
6) Publish the Code
If the code is worth writing, it is worth putting in a package and sharing publicly or privately within an organization. Mono-repo tools like lerna make it easy to deliver your code as an installable package.
How Test Coverage Reports Can Fail You
One of the things I realized when providing a list of all possible parent paths is that the path itself not only needs to be an absolute path, but the path needs to be normalized. What should the function do if someone provides a non-normalized path like /cat/../and/mouse
?
If I add the following line of code, we still get 100% test coverage but no longer have 100% feature coverage!
export const parentPaths: ParentPathsSignature = (
childPath: string,
): string[] => {
const normalizedPath = normalize(childPath);
//...
};
We've coded the feature to normalize the path, but we have no test coverage! Remember, if we remove any code, the tests should fail. Write the test to cover the new feature and then write the code.
Concerns
This approach seems to take a lot of work!
Ya. It is. But writing code should be difficult. Code is terrible: the less code, the better.
Following the suggested approach, you are more inclined to find an existing solution or try to design away the need to write the code in the first place.