End-to-end TypeScript project setup for 2026.
Layout (library)
mylib/
├── package.json
├── tsconfig.json
├── tsup.config.ts
├── vitest.config.ts
├── eslint.config.js
├── .prettierrc
├── .github/workflows/ci.yml
├── src/
│ ├── index.ts
│ └── ...
└── test/
└── *.test.ts
Init
npm init -y
npm i -D typescript tsup tsx vitest @vitest/coverage-v8 \
eslint @eslint/js typescript-eslint eslint-config-prettier \
prettier
package.json (library)
{
"name": "mylib",
"version": "0.1.0",
"type": "module",
"main": "./dist/index.cjs",
"module": "./dist/index.js",
"types": "./dist/index.d.ts",
"exports": {
".": {
"import": { "types": "./dist/index.d.ts", "default": "./dist/index.js" },
"require": { "types": "./dist/index.d.cts", "default": "./dist/index.cjs" }
}
},
"files": ["dist"],
"sideEffects": false,
"scripts": {
"dev": "tsup --watch",
"build": "tsup",
"typecheck": "tsc --noEmit",
"test": "vitest",
"test:run": "vitest run --coverage",
"lint": "eslint .",
"format": "prettier --write .",
"prepublishOnly": "npm run build"
},
"engines": { "node": ">=20" }
}
tsconfig.json
{
"compilerOptions": {
"target": "ES2023",
"module": "ESNext",
"moduleResolution": "Bundler",
"lib": ["ES2023"],
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"isolatedModules": true,
"verbatimModuleSyntax": true,
"noUncheckedIndexedAccess": true,
"noImplicitOverride": true,
"noFallthroughCasesInSwitch": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"declaration": true,
"declarationMap": true,
"sourceMap": true,
"outDir": "dist",
"noEmit": true
},
"include": ["src/**/*", "test/**/*"]
}
(noEmit: true since tsup emits — tsc only type-checks.)
tsup.config.ts
import { defineConfig } from "tsup";
export default defineConfig({
entry: ["src/index.ts"],
format: ["esm", "cjs"],
dts: true,
clean: true,
target: "node20",
sourcemap: true,
splitting: false,
});
vitest.config.ts
import { defineConfig } from "vitest/config";
export default defineConfig({
test: {
globals: true,
environment: "node",
coverage: {
provider: "v8",
reporter: ["text", "html"],
thresholds: {
lines: 80, branches: 80, functions: 80, statements: 80,
},
include: ["src/**"],
},
},
});
eslint.config.js (flat)
import js from "@eslint/js";
import ts from "typescript-eslint";
import prettier from "eslint-config-prettier";
export default ts.config(
js.configs.recommended,
...ts.configs.strictTypeChecked,
...ts.configs.stylisticTypeChecked,
{
languageOptions: {
parserOptions: {
projectService: true,
tsconfigRootDir: import.meta.dirname,
},
},
},
{
rules: {
"@typescript-eslint/no-unused-vars": ["error", { argsIgnorePattern: "^_" }],
},
},
prettier,
{ ignores: ["dist", "coverage", "node_modules"] },
);
.prettierrc
{
"semi": true,
"singleQuote": false,
"trailingComma": "all",
"printWidth": 100
}
GitHub Actions
.github/workflows/ci.yml:
name: CI
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
node: ["20", "22"]
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node }}
cache: npm
- run: npm ci
- run: npm run typecheck
- run: npm run lint
- run: npm run test:run
- run: npm run build
.gitignore
node_modules/
dist/
coverage/
.env
*.log
.DS_Store
.npmignore (or use “files” in package.json)
# Better: use "files" in package.json — whitelist > blacklist
Editor
.editorconfig:
root = true
[*]
charset = utf-8
end_of_line = lf
indent_style = space
indent_size = 2
trim_trailing_whitespace = true
insert_final_newline = true
.vscode/settings.json:
{
"editor.formatOnSave": true,
"editor.codeActionsOnSave": {
"source.fixAll.eslint": "explicit"
},
"typescript.tsdk": "node_modules/typescript/lib"
}
Pre-commit (lint-staged + husky)
npm i -D husky lint-staged
npx husky init
.husky/pre-commit:
npx lint-staged
package.json:
{
"lint-staged": {
"*.{ts,tsx}": ["eslint --fix", "prettier --write"],
"*.{json,md,yml}": ["prettier --write"]
}
}
Publish
npm run build
npm publish --access public
For pre-releases:
npm version prerelease --preid=beta
npm publish --tag beta
Common conventions
src/for source,dist/for build output.- Single-source-of-truth:
package.jsonwith"exports". tsc --noEmitfor typecheck, tsup for build.- ESM-first; ship CJS only if consumers need it.
- Pin Node version in
engines+ CI matrix. - Strict TS from day one.
Read this next
That’s 20 TypeScript cheatsheets. Next category: React.
If you want my full TS starter (lib + app variants wired), it’s at rajpoot.dev .
Building something AI-, backend-, or data-heavy and want a second pair of eyes? I do consulting and freelance work — see my projects and ways to reach me at rajpoot.dev .