Project template for TypeScript libraries optimized for tree-shaking.
- ESM-only with proper
exportsconfiguration - Tree-shaking optimized (see Tree Shaking)
- Types: TypeScript via tsgo
- Tests: Vitest
- Linting: oxlint + actionlint
- Formatting: dprint
- Package validation: publint + attw
- Publishing: Dripip
- CI: GitHub Actions
corepack enablegh repo create mylib --template jasonkuhrt/template-typescript-lib --clone --public && \
cd mylib && \
pnpm install && \
pnpm bootstrapThen setup a repo secret called NPM_TOKEN for CI publishing.
gh repo clone jasonkuhrt/template-typescript-lib mylib && \
cd mylib && \
pnpm install && \
pnpm bootstrapThis template is configured for aggressive tree-shaking. Bundlers (webpack, esbuild, rollup, vite) can eliminate unused code when consumers import from your library.
| Field | Value | Purpose |
|---|---|---|
type |
"module" |
ESM output (required for tree-shaking) |
sideEffects |
false |
Tells bundlers all modules are pure |
exports |
Explicit entry points | Black-boxes package internals |
| Option | Purpose |
|---|---|
verbatimModuleSyntax |
Preserves ES module syntax for bundlers |
isolatedModules |
Ensures code is compatible with single-file transpilers |
Two tools validate your package works correctly for consumers:
- publint - Checks packaging for compatibility across environments
- attw - Checks TypeScript types resolve correctly across module resolution modes
Run both with pnpm check.
Named exports tree-shake more reliably than default exports. Default exports can cause issues with CommonJS interop.
// Preferred
export const foo = () => {}
export const bar = () => {}
// Avoid
export default { foo, bar }For module-level function calls (HOCs, factories), the /*#__PURE__*/ annotation tells bundlers the call is side-effect free. See Terser and UglifyJS docs.
Bundlers cannot always statically determine if code has side effects. The sideEffects field hints to bundlers that your modules are "pure" and safe to prune if unused.
Important: If you add modules with side effects (e.g., CSS imports, polyfills, or code that runs on import), update sideEffects to an array:
{
"sideEffects": ["./src/polyfill.js", "**/*.css"]
}Barrel files (index.ts files that re-export from other modules) can inhibit tree-shaking in some bundlers. This template uses a single entry point which is acceptable for small libraries.
For larger libraries, consider:
- Multiple entry points via
exportsfield - Avoiding deep re-export chains
- Testing your bundle size with bundlephobia or pkg-size
- Webpack Tree Shaking Guide
- Tree-Shaking: A Reference Guide (Smashing Magazine)
- package.json exports field (Node.js)
- Building TypeScript Libraries (Arrange Act Assert)
- Are The Types Wrong?
- Strict settings via
@tsconfig/strictest - Node 22 target via
@tsconfig/node22 - Build cache in
node_modules/.cache - Output includes
declaration,declarationMap,sourceMapfor optimal consumer DX - Source published for go-to-definition support
- oxlint: Rust-based, ~100x faster than ESLint
- actionlint: GitHub Actions workflow validation
Vitest - fast, ESM-native test runner.
dprint - fast Rust-based formatter.
Parallel execution via pnpm pattern matching:
| Script | Description |
|---|---|
check |
Run all checks in parallel |
check:format |
Verify formatting |
check:lint |
Run oxlint |
check:types |
Type check with tsgo |
check:package |
Validate package with publint |
check:exports |
Validate exports with attw |
check:ci |
Lint GitHub Actions workflows |
fix |
Run all fixes in parallel |
fix:format |
Fix formatting |
fix:lint |
Auto-fix lint issues |
build |
Build with tsgo |
test |
Run tests |
PR workflow:
- actionlint, format, lint, types, publint checks
- Tests on ubuntu/macos/windows, Node 22
Trunk workflow:
- Automated canary release via Dripip
Configure tsgo globally in ~/.config/zed/settings.json:
{
"languages": {
"TypeScript": {
"language_servers": ["tsgo", "!vtsls", "oxc"]
}
}
}