Nice and simple Typescript library starter project which adds lots of value out of few tools. Based on recommendations by Mike North.
First, create a new directory and enter it
mkdir my-lib
cd my-lib
Then, create a .gitignore
file
npx gitignore node
and a package.json file
yarn init --yes
Make a few direct modifications to your package.json
file as follows
--- a/package.json
+++ b/package.json
@@ -1,6 +1,13 @@
{
"name": "my-lib",
"version": "1.0.0",
- "main": "index.js",
+ "main": "dist/index.js",
+ "types": "dist/index.d.ts",
+ "scripts": {
+ "build": "tsc",
+ "dev": "yarn build --watch --preserveWatchOutput",
+ "lint": "eslint src --ext js,ts",
+ "test": "jest"
+ },
"license": "MIT"
}
This ensures that TS and non-TS consumers alike can use this library, and that we can run the following commands
yarn build # build the project
yarn dev # build, and rebuild when source is changed
yarn lint # run the linter
yarn test # run tests
Pin the node and yarn versions to their current stable releases using volta
volta pin node yarn
this will add node
and yarn
versions to your package.json
automatically.
Next, initialize the git repository
git init
install typescript as a development dependency. We'll only need this at build time, because not all consumers of this library may be using TypeScript themselves.
yarn add -D typescript
Create a default tsconfig.json
yarn tsc --init
and ensure the following values are set:
"compilerOptions": {
+ "outDir": "dist",
+ "rootDirs": ["src"],
},
+ "include": ["src"]
We want to make sure that the src/
folder is where our source code lives, that it's treated as a root directory, and that the compiled output is in the dist/
folder.
Next, make sure that the TS compiler creates Node-friendly CommonJS modules, and that we target the ES2018 language level (Node 10, allowing for features like async
and await
).
"compilerOptions": {
+ "module": "commonjs",
+ "target": "ES2018",
}
Let's make sure two potentially problematic features are disabled. We'll talk later about why these are not great for a library.
"compilerOptions": {
+ "esModuleInterop": false,
+ "skipLibCheck": false
}
Make sure that the compiler outputs ambient type information in addition to the JavaScript
"compilerOptions": {
+ "declaration": true,
}
And finally, let's make sure that we set up an "extra strict" type-checking configuration
"compilerOptions": {
/**
* "strict": true,
* -------------------
* - noImplicitAny
* - strictNullChecks
* - strictFunctionTypes
* - strictBindCallApply
* - strictPropertyInitialization
* - noImplicitThis
* - alwaysStrict
*/
+ "strict": true,
+ "noUnusedLocals": true,
+ "noImplicitReturns": true,
+ "stripInternal": true,
+ "types": [],
+ "forceConsistentCasingInFileNames": true,
}
We'll go in to more detail later about what some of these options mean, and why I suggest setting them this way.
Finally, please create a folder for your source code, and create an empty index.ts
file within it
mkdir src
touch src/index.ts
Open src/index.ts
and set its contents to the following
/**
* @packageDocumentation A small library for common math functions
*/
/**
* Calculate the average of three numbers
*
* @param a - first number
* @param b - second number
* @param c - third number
*
* @public
*/
export function avg(a: number, b: number, c: number): number {
return sum3(a, b, c) / 3;
}
/**
* Calculate the sum of three numbers
*
* @param a - first number
* @param b - second number
* @param c - third number
*
* @beta
*/
export function sum3(a: number, b: number, c: number): number {
return sum2(a, sum2(b, c));
}
/**
* Calculate the sum of two numbers
*
* @param a - first number
* @param b - second number
*
* @internal
*/
export function sum2(a: number, b: number): number {
const sum = a + b;
return sum;
}
This is obviously convoluted, but it'll serve our purposes for looking at some interesting behavior later.
Let's make sure that things are working so far by trying to build this project.
rm -rf dist # clear away any old compiled output
yarn build # build the project
ls dist # list the contents of the dist/ folder
You should see something like
index.d.ts index.js
Install eslint as a development dependency
yarn add -D eslint
and go through the process of creating a starting point ESLint config file
yarn eslint --init
When asked, please answer as follows for the choices presented to you:
- How would you like to use ESLint?
- To check syntax and find problems
- What type of modules does your project use
- None of these
- Which framework does your project use?
- None of these
- Does your project use TypeScript?
- Yes
- Where does your code run?
- Node
- What format do you want your config file to be in?
- JSON
- Would you like to install them now with npm?
- Yes
Because we're using yarn
, let's delete that npm
file package-lock.json
and run yarn
to update yarn.lock
.
rm package-lock.json
yarn
Let's also enable a set of rules that take advantage of type-checking information
--- a/.eslintrc.json
+++ b/.eslintrc.json
@@ -5,7 +5,8 @@
},
"extends": [
"eslint:recommended",
- "plugin:@typescript-eslint/recommended"
+ "plugin:@typescript-eslint/recommended",
+ "plugin:@typescript-eslint/recommended-requiring-type-checking"
],
"parser": "@typescript-eslint/parser",
There's one rule we want to enable, and that's a preference for const
over let
. While we're here,
we can disable ESLint's rules for unused local variables and params, because the TS
compiler is responsible for telling us about those
--- a/.eslintrc.json
+++ b/.eslintrc.json
@@ -14,5 +14,6 @@
},
"plugins": ["@typescript-eslint"],
"rules": {
+ "prefer-const": "error",
+ "@typescript-eslint/no-unused-vars": "off",
+ "@typescript-eslint/no-unused-params": "off"
}
}
ESLint needs a single tsconfig file that includes our entire project (including tests), so we'll need to make a small dedicated one
{
"extends": "./tsconfig.json",
"compilerOptions": {
"types": ["jest"]
},
"include": ["src", "tests"]
}
Going back to our /.eslintrc.json
, we need to tell ESLint about this new TS config -- rules that require type-checking need to know about where it is
--- a/.eslintrc.json
+++ b/.eslintrc.json
@@ -4,14 +4,17 @@
"parserOptions": {
- "ecmaVersion": 12
+ "ecmaVersion": 12,
+ "project": "tsconfig.eslint.json"
},
}
While we're in here, let's set up some different rules for our test files compared to our source files
--- a/.eslintrc.json
+++ b/.eslintrc.json
@@ -15,5 +15,11 @@
"plugins": ["@typescript-eslint"],
"rules": {
"prefer-const": "error"
- }
+ },
+ "overrides": [
+ {
+ "files": "test/**/*.ts",
+ "env": { "node": true, "jest": true }
+ }
+ ]
}
Let's make sure this works by making a change to our src/index.ts
that breaks our prefer-const
rule
--- a/src/index.ts
+++ b/src/index.ts
@@ -33,5 +33,6 @@ export function sum3(a: number, b: number, c: number): number {
* @internal
*/
export function sum2(a: number, b: number): number {
- return a + b;
+ let sum = a + b;
+ return sum;
}
running
yarn lint
should tell us that this is a problem. If properly configured, you may also see feedback right in your code editor as well
Undo the problematic code change, run yarn lint
again and you should see no errors
Next, let's install our test runner, and associated type information, along with some required babel plugins
yarn add -D jest @types/jest @babel/preset-env @babel/preset-typescript
and make a folder for our tests
mkdir tests
Create a file to contain the tests for our src/index.ts
module
touch tests/index.test.ts
import { avg, sum3 } from "..";
describe("avg should calculate an average properly", () => {
test("three positive numbers", () => {
expect(avg(3, 4, 5)).toBe(4);
});
test("three negative numbers", () => {
expect(avg(3, -4, -5)).toBe(-2);
});
});
describe("sum3 should calculate a sum properly", () => {
test("three positive numbers", () => {
expect(sum3(3, 4, 5)).toBe(12);
});
test("three negative numbers", () => {
expect(sum3(3, -4, -5)).toBe(-6);
});
});
We'll need to make a one-line change in our existing /tsconfig.json
file
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -1,4 +1,5 @@
{
+ "composite": true,
"compilerOptions": {
and to create a small tests/tsconfig.json
just for our tests
{
"extends": "../tsconfig.json",
"references": [{ "name": "my-lib", "path": ".." }],
"compilerOptions": {
"types": ["jest"],
"rootDir": ".."
},
"include": ["."]
}
and a small little babel config at the root of our project, so that Jest can understand TypeScript
{
"presets": [
["@babel/preset-env", { "targets": { "node": "10" } }],
"@babel/preset-typescript"
]
}
At this point, we should make sure that everything works as intended before proceeding further.
Run
yarn test
to run the tests with jest. You should see some output like
PASS tests/index.test.ts
avg should calculate an average properly
✓ three positive numbers (2 ms)
✓ three negative numbers
sum3 should calculate a sum properly
✓ three positive numbers
✓ three negative numbers
Test Suites: 1 passed, 1 total
Tests: 4 passed, 4 total
Snapshots: 0 total
Time: 1.125 s
Ran all test suites.
✨ Done in 1.74s.
We're going to use Microsoft's api-extractor as our documentation tool -- but it's really much more than that as we'll see later
First, let's install it
yarn add -D @microsoft/api-extractor @microsoft/api-documenter
and let's ask api-extractor
to create a default config for us
yarn api-extractor init
This should result in a new file /api-extractor.json
being created. Open it
up and make the following changes
--- a/api-extractor.json
+++ b/api-extractor.json
@@ -45,7 +45,7 @@
*
* SUPPORTED TOKENS: <projectFolder>, <packageName>, <unscopedPackageName>
*/
- "mainEntryPointFilePath": "<projectFolder>/lib/index.d.ts",
+ "mainEntryPointFilePath": "<projectFolder>/dist/index.d.ts",^M
/**
* A list of NPM package names whose exports should be treated as part of this package.
@@ -181,7 +181,7 @@
/**
* (REQUIRED) Whether to generate the .d.ts rollup file.
*/
- "enabled": true
+ "enabled": true,^M
/**
* Specifies the output path for a .d.ts rollup file to be generated without any trimming.
@@ -195,7 +195,7 @@
* SUPPORTED TOKENS: <projectFolder>, <packageName>, <unscopedPackageName>
* DEFAULT VALUE: "<projectFolder>/dist/<unscopedPackageName>.d.ts"
*/
- // "untrimmedFilePath": "<projectFolder>/dist/<unscopedPackageName>.d.ts",
+ "untrimmedFilePath": "<projectFolder>/dist/<unscopedPackageName>-private.d.ts",
/**
* Specifies the output path for a .d.ts rollup file to be generated with trimming for a "beta" release.
@@ -207,7 +207,7 @@
* SUPPORTED TOKENS: <projectFolder>, <packageName>, <unscopedPackageName>
* DEFAULT VALUE: ""
*/
- // "betaTrimmedFilePath": "<projectFolder>/dist/<unscopedPackageName>-beta.d.ts",
+ "betaTrimmedFilePath": "<projectFolder>/dist/<unscopedPackageName>-beta.d.ts",
/**
* Specifies the output path for a .d.ts rollup file to be generated with trimming for a "public" release.
@@ -221,7 +221,7 @@
* SUPPORTED TOKENS: <projectFolder>, <packageName>, <unscopedPackageName>
* DEFAULT VALUE: ""
*/
- // "publicTrimmedFilePath": "<projectFolder>/dist/<unscopedPackageName>-public.d.ts",
+ "publicTrimmedFilePath": "<projectFolder>/dist/<unscopedPackageName>-public.d.ts"
/**
* When a declaration is trimmed, by default it will be replaced by a code comment such as
Make an empty /etc
folder
mkdir etc
and then run api-extractor for the first time
yarn api-extractor run --local
This should result in a new file being created: /etc/my-lib.api.md
. This is
your api-report. There's also a /temp
folder that will have been created. You
should add this to your .gitignore
.
--- a/.gitignore
+++ b/.gitignore
@@ -114,3 +114,5 @@ dist
.yarn/build-state.yml
.yarn/install-state.gz
.pnp.*
+
+temp
you may also notice that some new .d.ts
files are in your /dist
folder.
Take a look at the contents. Do you see anything interesting?
We can use api-documenter
to create markdown API docs by running
yarn api-documenter markdown -i temp -o docs
This should result in the creation of a /docs
folder containing the markdown pages.
Finally, we should make a couple of new npm scripts to help us easily
generate new docs by running api-extractor
and api-documenter
sequentially
--- a/package.json
+++ b/package.json
@@ -7,7 +7,10 @@
"build": "tsc",
"watch": "yarn build --watch --preserveWatchOutput",
"lint": "eslint src tests --ext ts,js",
- "test": "jest"
+ "test": "jest",
+ "api-report": "api-extractor run",
+ "api-docs": "api-documenter markdown -i temp -o docs",
+ "build-with-docs": "yarn build && yarn api-report && yarn api-docs"
},
"license": "MIT",
"volta": {
Make a commit so you have a clean workspace.
git commit -am "setup api-extractor and api-documenter"
Let's "enhance" our library by supporting a fourth number in sum3()
--- a/src/index.ts
+++ b/src/index.ts
@@ -21,11 +21,12 @@ export function avg(a: number, b: number, c: number): number {
* @param a - first number
* @param b - second number
* @param c - third number
+ * @param d - fourth number
*
* @beta
*/
-export function sum3(a: number, b: number, c: number): number {
- return sum2(a, sum2(b, c));
+export function sum3(a: number, b: number, c: number, d = 0): number {
+ return sum2(a, b) + sum2(c, d);
}
Now run
yarn build-with-docs
You should see something like
Warning: You have changed the public API signature for this project. Please copy the file "temp/my-lib.api.md" to "etc/my-lib.api.md", or perform a local build (which does this automatically). See the Git repo documentation for more info.
This is api-extractor
telling you that something that users can observe
through the public API surface of this library has changed. We can follow its instructions
to indicate that this was an intentional change (and probably a minor release instead of a patch)
cp temp/my-lib.api.md etc
and build the docs again
yarn build-with-docs
You should now see an updated api-report. It's now very easy to see the ramifications of changes to our API surface on a per-code-change basis!
diff --git a/etc/my-lib.api.md b/etc/my-lib.api.md
index fc8ea25..82c4ac4 100644
--- a/etc/my-lib.api.md
+++ b/etc/my-lib.api.md
@@ -8,7 +8,7 @@
export function avg(a: number, b: number, c: number): number;
// @beta
-export function sum3(a: number, b: number, c: number): number;
+export function sum3(a: number, b: number, c: number, d?: number): number;
Our documentation has also been updated automatically
--- a/docs/my-lib.md
+++ b/docs/my-lib.md
@@ -11,5 +11,5 @@ A small library for common math functions
| Function | Description |
| --- | --- |
| [avg(a, b, c)](./my-lib.avg.md) | Calculate the average of three numbers |
-| [sum3(a, b, c)](./my-lib.sum3.md) | <b><i>(BETA)</i></b> Calculate the sum of three numbers |
+| [sum3(a, b, c, d)](./my-lib.sum3.md) | <b><i>(BETA)</i></b> Calculate the sum of three numbers |
diff --git a/docs/my-lib.sum3.md b/docs/my-lib.sum3.md
index 8ab69a1..4ca8888 100644
--- a/docs/my-lib.sum3.md
+++ b/docs/my-lib.sum3.md
@@ -12,7 +12,7 @@ Calculate the sum of three numbers
<b>Signature:</b>
` ``typescript
-export declare function sum3(a: number, b: number, c: number): number;
+export declare function sum3(a: number, b: number, c: number, d?: number): number;
` ``
## Parameters
@@ -22,6 +22,7 @@ export declare function sum3(a: number, b: number, c: number): number;
| a | number | first number |
| b | number | second number |
| c | number | third number |
+| d | number | fourth number |
<b>Returns:</b>
Congrats! we now have
- Compiling to JS
- Linting
- Tests
- Docs
- API surface change detection
without having to reach for more complicated tools like webpack!