Skip to content

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" }
}
  • files keeps src/, tests and node_modules out of the tarball; run npm pack --dry-run and read the listing before every release.
  • exports + types are what make import { kwh } from '@yourname/solar-units' work with full typing for both ESM consumers and editors.
  • prepublishOnly is the last gate: type-check, tests, build. A published version can be deprecated, never deleted β€” hence semver discipline (major for a breaking change of a published type, minor for a new export, patch for 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

  1. Publish a tiny package for real. Create solar-units with 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;
}
  1. 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 that kwh(-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.

  1. 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.

  2. Publish to the public registry only if you want to β€” npm publish --access public requires an account and cannot be undone. For practice, npm pack and 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, types and prepublishOnly are 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

  1. Where do you look to find out whether a function parameter is optional β€” the README or something else?
  2. What does a branded type prevent that a plain number does not, and what does it cost?
  3. Which four package.json fields decide what a consumer installs and what they can import?
  4. Why is prepublishOnly a better place for the type-check than a habit of remembering to run it?
  5. Of the tools in the ecosystem map, which two would you pick for a new project tomorrow, and why?

Next

Start Here β€” the course map

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.