44 β Libraries, Publishing and What to Learn Next
Level: Advanced Β· Time: ~30 min Β· Prerequisites: Lesson 43 β Type-Safety Discipline and Common Traps
Why this matters
You can now read code you did not write, make the compiler check your assumptions, and ship an application that touches files, APIs and a database. The remaining skill is the one that keeps you growing without a course: reading types as documentation. A library's .d.ts file tells you what its authors promise β which arguments are optional, which errors it can throw, which of its generics you are allowed to narrow. Add publishing your own package and you stop being only a consumer.
This closing lesson is deliberately dense: the patterns you will meet in real codebases, the ecosystem in one map, and two reference tables β a glossary of the course and a cheat sheet of the syntax you will type every day.
Reading a library's types as documentation
| Reflex | What it gives you |
|---|---|
Go to definition (F12, or gd in an editor with LSP) |
The real signature, not the README's summary |
Hover with Ctrl |
Inferred type at a call site, including generic substitutions |
Peek the .d.ts (node_modules/<pkg>/dist/index.d.ts) |
The public surface: what is exported, what is optional, what is internal |
| Read the generic from the call site | parse<T>(s: string): T means you choose T β and you are promising the string is valid |
Two habits pay immediately. First, when a call fails to type-check, read the error's two types (Type 'X' is not assignable to type 'Y') before touching the code β the compiler is telling you which contract you broke. Second, when a signature has more than three generic parameters, look for the intent: Promise<Result<T, E>> says asynchronous, may fail, holds a value or an error, which is documentation you would otherwise write in prose.
// The signature is the manual: TData is inferred from fn, TError from the mapper
declare function attempt<TData, TError>(
fn: () => Promise<TData>,
onError: (cause: unknown) => TError,
): Promise<{ ok: true; data: TData } | { ok: false; error: TError }>;
Advanced patterns you will meet β and when not to use them
These appear in library code and in large codebases. Know them; reach for them rarely.
| Pattern | What it does | Use it when | Avoid it when |
|---|---|---|---|
Mapped types ({ [K in keyof T]: β¦ }) |
Transforms every property of a type | Building derived API/DTO shapes once, in shared code | You need it in three places to save six lines |
Conditional types (T extends U ? A : B) |
Chooses a type by a condition | A library must stay generic across input kinds | A union in the domain would be clearer |
infer |
Captures a type inside a conditional | Extracting T from Promise<T> or a function's return |
You are the only reader of it |
Template literal types (`${string}/${string}`) |
Types strings by shape | Route paths, event names, unit strings | The value comes from a runtime source anyway |
Branded types (type Kwh = number & { __brand: 'kwh' }) |
Prevents mixing same-shaped units | Money, energy units, identifiers that look alike | A simple wrapper class is equally clear |
satisfies |
Checks a literal against a type, keeps inference | Config objects, route tables | A plain annotation already works |
The rule of three applies to types as much as to code: do not abstract until the same shape has appeared three times.
Publishing your own package
A publishable package is a small contract: what to publish, how to be imported, and what a consumer needs.
{
"name": "@yourname/solar-units",
"version": "0.1.0",
"type": "module",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
"exports": { ".": { "types": "./dist/index.d.ts", "default": "./dist/index.js" } },
"files": ["dist"],
"scripts": { "build": "tsc -p tsconfig.build.json", "prepublishOnly": "npm run typecheck && npm test && npm run build" }
}
fileskeepssrc/, tests andnode_modulesout of the tarball; runnpm pack --dry-runand read the listing before every release.exports+typesare what makeimport { kwh } from '@yourname/solar-units'work with full typing for both ESM consumers and editors.prepublishOnlyis the last gate: type-check, tests, build. A published version can be deprecated, never deleted β hence semver discipline (majorfor a breaking change of a published type,minorfor a new export,patchfor a fix).- Documentation that helps: a README with what the package does, one install line, one minimal example that actually runs, and the public API listed with its types. JSDoc adds value only where the same-name function is not obvious:
/** Parses '2024-06-01' as a UTC date; throws RangeError on any other format. */.
The ecosystem in one map
| Tool | What it is for | Reach for it when |
|---|---|---|
| Node.js | The default runtime for servers and tools | Everything in this course |
| Deno / Bun | Alternative runtimes with TypeScript closer to the metal | A new small service; you accept a smaller ecosystem |
| Vite | Dev server and build tool for the browser | Any front end, framework or not |
| Express / Fastify | HTTP frameworks for the backend | You need routes, middleware and validation |
| zod | Runtime schema validation inferred into types | Every boundary of every application |
| Prisma | Typed database access and migrations | You want SQL without hand-written queries |
| Vitest | Test runner with TypeScript support out of the box | From the first function you write |
| tRPC | End-to-end type safety between your own front end and backend | One team, one codebase, no public API |
| Next.js / Nest | Full application frameworks (React SSR / structured backend) | You are building a product, not learning, and accept the abstraction |
| ESLint + Prettier | Rules and formatting, enforced in CI | Team work, or to stop arguing about style |
Glossary
| Term | Meaning |
|---|---|
| Annotation | A type written by hand, typically on a parameter or a return value |
| Inference | The compiler deducing a type you did not write |
| Narrowing | Reducing a union to fewer members using checks the compiler understands |
| Discriminated union | A union whose members share a literal field used to tell them apart |
| Type guard | A function whose return type is x is T, teaching the compiler a check |
| Assertion function | A function declared asserts x is T that throws when the claim is false |
| Exhaustiveness | Handling every union member; proved by assigning the remainder to never |
| Generic | A type parameter, so one implementation keeps the caller's type |
| Constraint | T extends U, limiting what a generic may be |
| Utility type | A built-in type transformation: Partial, Pick, Omit, Record, Awaited |
| Mapped type | A type built by iterating over another type's keys |
| Conditional type | A type chosen by a condition, often with infer |
| Structural typing | Compatibility by shape, not by name |
| Excess property check | The stricter rule applied to fresh object literals |
Declaration file (.d.ts) |
Type-only description of JavaScript code |
@types/* |
Community-maintained declarations for untyped packages |
| Module (ESM) | import/export units; the modern standard |
| CommonJS | The older require/module.exports system Node still supports |
tsconfig.json |
The compiler configuration: target, module, strictness, paths |
strict |
The umbrella flag enabling the safety checks a beginner needs |
| Declaration merging | Interfaces with the same name combining into one |
| Type alias | A name for any type: union, tuple, primitive or object |
| Interface | A named object contract that can be extended and merged |
| Abstract class | A class that cannot be instantiated and may declare unimplemented members |
| Dependency injection | Passing collaborators in rather than constructing them inside |
| Value object | An immutable object defined by its values, validated at creation |
| Repository | An interface exposing domain-level data operations, hiding the store |
| Port / adapter | A contract and its implementation, so infrastructure can be swapped |
| DTO | The shape of data at an interface boundary, distinct from the domain type |
| Schema validation | Runtime checking of external data (zod or similar) |
| Promise | A value that will exist later: pending, fulfilled or rejected |
async/await |
Syntax for working with promises as if they were sequential |
| Microtask | A promise continuation, run before timers |
| Event loop | The mechanism giving Node concurrency on one JavaScript thread |
| Blocking | Synchronous work that stops the loop and every pending request |
AbortController |
The standard way to cancel fetch and other operations |
| Backoff with jitter | Delaying retries by a growing, randomised interval |
| Idempotent | An operation that can be repeated without changing the result |
| Atomic write | Writing to a temporary file then renaming, so no partial file is visible |
| Transaction | A group of database operations that all succeed or all fail |
| Repository pattern | Separating data access from business logic behind an interface |
| Migration | A versioned, repeatable change to a database schema |
| Seed | Idempotent insertion of reference or test data |
| CORS | The browser rule that a server must opt in to cross-origin calls |
| Preflight | The automatic OPTIONS request the browser sends before some calls |
| Reverse proxy | The server terminating TLS and forwarding to your application |
| Health check | An endpoint proving the service and its dependencies work |
| CI | Automated type-check, lint and tests on every push |
| Semver | Version numbering: major, minor, patch, with the contract they imply |
Cheat sheet
// Values and types
const plant = "Sokone"; // type: "Sokone"
let count = 0; // type: number
type Status = "idle" | "loading" | "done";
const check: Record<string, number> = {};
// Functions
function kwh(wh: number, hours = 1): number { return wh * hours; }
const area = (w: number, h: number): number => w * h;
function first<T>(items: readonly T[]): T | undefined { return items[0]; }
// Objects and destructuring
type Reading = { readonly date: string; readonly kwh: number; site?: string };
const { date, kwh } = reading;
const next = { ...reading, kwh: reading.kwh + 1 };
// Narrowing
if (typeof value === "string") { /* string */ }
if (value instanceof Error) { /* Error */ }
switch (result.kind) {
case "loaded": return result.rows;
case "error": throw new Error(result.message);
default: { const _never: never = result; return _never; }
}
// Classes and contracts
class Meter implements Measurable { constructor(private readonly id: string) {} }
interface Storage<T> { save(item: T): Promise<void>; all(): Promise<readonly T[]>; }
// Async
const rows = await fetch(url, { signal: AbortSignal.timeout(5000) }).then(r => r.json());
const [a, b] = await Promise.all([loadA(), loadB()]);
const settled = await Promise.allSettled(tasks);
// Utility types
type Update = Partial<Reading>;
type Public = Omit<Reading, "site">;
type Key = keyof Reading; // "date" | "kwh" | "site"
type Value = Reading[Key]; // string | number | undefined
Common mistakes
| Mistake | What happens | The fix |
|---|---|---|
Publishing without files |
The tarball ships src/, tests and secrets lists, and is huge |
npm pack --dry-run on every release; keep files: ["dist"] |
Shipping types but not the types field |
Consumers get any and lose every benefit |
Build declarations, point types/exports at them, test with a scratch consumer |
| Reaching for mapped and conditional types too early | Nobody, including you in a month, can read the code | Rule of three: abstract only after the third repetition |
| Documenting the obvious | JSDoc that repeats the signature is noise that will drift | Document units, errors, side effects and non-obvious formats |
| Following a framework's abstractions instead of your goal | You learn the framework, not the language | Finish a project with zero frameworks first β you have just done that |
| Treating the course as over | Skills decay; the syntax you do not type disappears | Keep one small project alive and extend it monthly |
| Chasing every new runtime and tool | Context switching with nothing shipped | One runtime (Node), one test runner (Vitest), one validator (zod) until a real limit appears |
Try it
- Publish a tiny package for real. Create
solar-unitswith one exported function and a branded unit type:
export type Kwh = number & { readonly __brand: "kwh" };
export function kwh(value: number): Kwh {
if (!Number.isFinite(value) || value < 0) throw new RangeError("kwh expects a finite, non-negative number");
return value as Kwh;
}
- Build it, inspect the output (
npm pack --dry-run), and consume it from a second project by path (npm install ../solar-units). Confirm autocompletion and thatkwh(-1)throws at runtime.
Expected output: the dry-run lists dist/index.js, dist/index.d.ts, package.json and README.md β and nothing else.
-
Read one library's types deliberately: open
node_modules/zod/lib/types.d.ts(or any installed package), find a generic signature, and write down in one sentence what it promises. That sentence is the documentation the README did not give you. -
Publish to the public registry only if you want to β
npm publish --access publicrequires an account and cannot be undone. For practice,npm packand installing the tarball teaches the same lesson with no consequences.
Key takeaways
- Types are documentation that cannot go stale: read signatures before READMEs, and let error messages tell you which contract you broke.
- Advanced types are for libraries, not for applications: mapped, conditional and branded types earn their complexity only where the same shape repeats three times.
- A published package is a contract β
files,exports,typesandprepublishOnlyare the four lines that make it a professional one. - The ecosystem map is finite: Node, Vite, a server framework, zod, Prisma, Vitest and a linter cover the overwhelming majority of real work.
- The course ends, the habit does not: keep shipping small TypeScript projects, and re-read the release notes when a new version changes what the compiler accepts.
Check yourself
- Where do you look to find out whether a function parameter is optional β the README or something else?
- What does a branded type prevent that a plain
numberdoes not, and what does it cost? - Which four
package.jsonfields decide what a consumer installs and what they can import? - Why is
prepublishOnlya better place for the type-check than a habit of remembering to run it? - Of the tools in the ecosystem map, which two would you pick for a new project tomorrow, and why?
Next
You have finished the path. Rebuild one of the two applications from memory β the console importer or the small full-stack app β without looking at the lessons. What you can rebuild is what you know; what you cannot is your reading list for next month.