Prerequisites
- Basic understanding of JavaScript 📝
- TypeScript installation ⚡
- VS Code or preferred IDE 💻
What you'll learn
- Understand pnpm fundamentals 🎯
- Apply pnpm in real projects 🏗️
- Debug common pnpm issues 🐛
- Write type-safe package configurations ✨
🎯 Introduction
Welcome to the supercharged world of pnpm! 🎉 In this guide, we’ll explore the blazing-fast package manager that’s revolutionizing TypeScript development.
You’ll discover how pnpm can transform your development experience with lightning-fast installs ⚡, disk space savings 💾, and bulletproof dependency management. Whether you’re building web applications 🌐, server-side code 🖥️, or libraries 📚, mastering pnpm is essential for modern TypeScript development.
By the end of this tutorial, you’ll be a pnpm power user ready to supercharge your projects! Let’s dive in! 🏊♂️
📚 Understanding PNPM
🤔 What is PNPM?
PNPM is like a master librarian 📚 who organizes books so efficiently that multiple libraries can share the same books without duplication! Think of it as a smart package manager that creates a single global store and uses symbolic links to share dependencies across projects.
In TypeScript terms, pnpm creates a content-addressable store where packages are stored once and linked everywhere they’re needed 🔗. This means you can:
- ✨ Save massive amounts of disk space
- 🚀 Install dependencies lightning fast
- 🛡️ Maintain strict dependency isolation
- 💰 Reduce bandwidth usage
💡 Why Use PNPM?
Here’s why developers are switching to pnpm:
- Speed ⚡: Up to 2x faster than npm/yarn
- Disk Efficiency 💾: Uses 1/3 the space of npm
- Strict Dependencies 🛡️: Prevents phantom dependencies
- Monorepo Support 🏗️: Built-in workspace management
- Drop-in Replacement 🔄: Compatible with npm scripts
Real-world example: Imagine building a TypeScript e-commerce site 🛒. With pnpm, your node_modules is lean, installs are instant, and you never accidentally import packages that aren’t declared!
🔧 Basic Syntax and Usage
📝 Installation & Setup
Let’s get pnpm up and running:
# 🌟 Install pnpm globally
npm install -g pnpm
# 🎯 Verify installation
pnpm --version
# 🚀 Alternative: Use with Node.js corepack
corepack enable
corepack prepare pnpm@latest --activate
💡 Pro Tip: Use corepack for automatic version management per project!
🎯 Common Commands
Here are the commands you’ll use daily:
# 📦 Initialize a new project
pnpm init
# ➕ Install dependencies
pnpm install
pnpm add typescript @types/node # 📘 Add packages
pnpm add -D eslint prettier # 🛠️ Dev dependencies
pnpm add -g ts-node # 🌍 Global packages
# 🔄 Update packages
pnpm update # Update all
pnpm update typescript # Update specific
# ❌ Remove packages
pnpm remove lodash # Remove package
pnpm remove -D @types/jest # Remove dev dependency
# 🧹 Clean up
pnpm prune # Remove unused packages
💡 Practical Examples
🛒 Example 1: TypeScript E-commerce Project
Let’s set up a real TypeScript project with pnpm:
// 📝 package.json - E-commerce store
{
"name": "typescript-store",
"version": "1.0.0",
"description": "🛒 TypeScript e-commerce platform",
"scripts": {
"dev": "ts-node src/server.ts",
"build": "tsc",
"start": "node dist/server.js",
"test": "jest",
"lint": "eslint src/**/*.ts"
},
"dependencies": {
"express": "^4.18.2",
"cors": "^2.8.5",
"dotenv": "^16.0.3"
},
"devDependencies": {
"typescript": "^5.0.0",
"@types/express": "^4.17.17",
"@types/cors": "^2.8.13",
"@types/node": "^18.15.0",
"ts-node": "^10.9.0",
"eslint": "^8.36.0"
}
}
// 🏗️ TypeScript project structure
interface Product {
id: string;
name: string;
price: number;
emoji: string; // 🎨 Every product needs personality!
}
class Store {
private products: Product[] = [];
// ➕ Add product
addProduct(product: Product): void {
this.products.push(product);
console.log(`✅ Added ${product.emoji} ${product.name} to store!`);
}
// 🔍 Find products
findByName(name: string): Product | undefined {
return this.products.find(p =>
p.name.toLowerCase().includes(name.toLowerCase())
);
}
// 💰 Get total inventory value
getTotalValue(): number {
return this.products.reduce((sum, product) => sum + product.price, 0);
}
}
// 🎮 Initialize store
const myStore = new Store();
myStore.addProduct({
id: "1",
name: "TypeScript Handbook",
price: 39.99,
emoji: "📘"
});
🚀 pnpm Commands for this project:
# 🏗️ Set up project
pnpm init
pnpm add express cors dotenv
pnpm add -D typescript @types/express @types/cors @types/node ts-node eslint
# 🔥 Run development server
pnpm dev
# 📦 Build for production
pnpm build
🎮 Example 2: Monorepo Game Development
Let’s create a TypeScript game with multiple packages:
# 📁 Project structure
game-monorepo/
├── package.json # 🎯 Root config
├── pnpm-workspace.yaml # 🏗️ Workspace config
├── packages/
│ ├── game-engine/ # 🎮 Core engine
│ ├── ui-components/ # 🎨 UI library
│ └── game-client/ # 🖥️ Client app
# 🏗️ pnpm-workspace.yaml
packages:
- 'packages/*'
// 📦 Root package.json
{
"name": "typescript-game-monorepo",
"private": true,
"scripts": {
"build": "pnpm -r build", // 🔄 Build all packages
"dev": "pnpm -r --parallel dev", // 🚀 Run all in parallel
"test": "pnpm -r test" // 🧪 Test everything
},
"devDependencies": {
"typescript": "^5.0.0",
"@typescript-eslint/eslint-plugin": "^5.55.0"
}
}
// 🎮 packages/game-engine/src/Engine.ts
export interface GameEntity {
id: string;
x: number;
y: number;
emoji: string; // 🎨 Visual representation
}
export class GameEngine {
private entities: Map<string, GameEntity> = new Map();
// ➕ Spawn entity
spawn(entity: GameEntity): void {
this.entities.set(entity.id, entity);
console.log(`🌟 Spawned ${entity.emoji} at (${entity.x}, ${entity.y})`);
}
// 🎯 Update game loop
update(deltaTime: number): void {
for (const [id, entity] of this.entities) {
// 🔄 Update entity positions
this.updateEntity(entity, deltaTime);
}
}
private updateEntity(entity: GameEntity, deltaTime: number): void {
// 🎮 Game logic here
console.log(`⚡ Updating ${entity.emoji}`);
}
}
🔥 Monorepo pnpm commands:
# 🏗️ Install all workspace dependencies
pnpm install
# 🎯 Add dependency to specific package
pnpm add lodash --filter game-engine
# 🚀 Run script in all packages
pnpm -r build
# 🎮 Run specific package
pnpm --filter game-client dev
🚀 Advanced Concepts
🧙♂️ Advanced Configuration: .npmrc
When you’re ready to level up, customize pnpm behavior:
# 📝 .npmrc - Project-specific config
# 🚀 Performance optimizations
shamefully-hoist=false # Strict mode - prevent phantom deps
strict-peer-dependencies=false # Allow peer dep conflicts
auto-install-peers=true # Auto-install missing peers
# 💾 Storage settings
store-dir=~/.pnpm-store # Custom store location
cache-dir=~/.pnpm-cache # Custom cache location
# 🔗 Registry settings
registry=https://registry.npmjs.org/
@mycompany:registry=https://npm.mycompany.com/
# 🛡️ Security settings
audit-level=moderate # Security audit threshold
🏗️ Advanced Workspace Management
For complex monorepos:
# 🎯 Advanced pnpm-workspace.yaml
packages:
- 'packages/*'
- 'apps/*'
- '!**/test/**' # Exclude test directories
- 'tools/*'
// 📦 Advanced package.json scripts
{
"scripts": {
"build:libs": "pnpm --filter './packages/*' build",
"build:apps": "pnpm --filter './apps/*' build",
"test:changed": "pnpm --filter '...[HEAD~1]' test",
"lint:all": "pnpm -r --parallel lint",
"clean": "pnpm -r exec -- rm -rf dist node_modules"
}
}
// 🎨 Type-safe workspace configuration
interface WorkspaceConfig {
packages: string[];
publishConfig?: {
registry: string;
access: 'public' | 'restricted';
};
pnpm?: {
overrides?: Record<string, string>;
peerDependencyRules?: {
allowedVersions?: Record<string, string>;
ignoreMissing?: string[];
};
};
}
const workspaceConfig: WorkspaceConfig = {
packages: ['packages/*', 'apps/*'],
publishConfig: {
registry: 'https://registry.npmjs.org/',
access: 'public'
},
pnpm: {
overrides: {
'lodash': '4.17.21' // 🔒 Force specific version
},
peerDependencyRules: {
ignoreMissing: ['@types/react'] // 🤷♂️ Ignore missing peer deps
}
}
};
⚠️ Common Pitfalls and Solutions
😱 Pitfall 1: Phantom Dependencies
// ❌ Wrong - importing undeclared dependency
import _ from 'lodash'; // 💥 Error! lodash not in package.json
// This works in npm but fails in pnpm (which is good!)
const users = _.filter(data, user => user.active);
# ✅ Correct - explicitly declare dependencies
pnpm add lodash
pnpm add -D @types/lodash # 📘 TypeScript types
// ✅ Now it works and is properly declared
import _ from 'lodash'; // ✅ Safe and explicit
const users = _.filter(data, user => user.active);
🤯 Pitfall 2: Peer Dependency Issues
# ⚠️ Common error message
ERR_PNPM_PEER_DEP_ISSUES Unmet peer dependency
# ✅ Solutions:
# Option 1: Install the peer dependency
pnpm add react react-dom
# Option 2: Configure to auto-install
echo "auto-install-peers=true" >> .npmrc
# Option 3: Ignore specific peer deps
echo "pnpm.peerDependencyRules.ignoreMissing=@types/react" >> package.json
😵 Pitfall 3: Hoisting Issues
# ❌ Module not found errors in pnpm
Module '"../node_modules/some-package"' not found
# ✅ Fix with proper imports
# Don't import from node_modules directly
import something from '../node_modules/package'; // ❌ Bad
# Import from package name
import something from 'package'; // ✅ Good
🛠️ Best Practices
- 🎯 Use .npmrc: Configure pnpm behavior per project
- 📝 Declare Dependencies: Never rely on phantom dependencies
- 🛡️ Enable Strict Mode: Keep
shamefully-hoist=false
- 🏗️ Organize Workspaces: Use clear workspace patterns
- ✨ Cache Smartly: Use
pnpm store path
to manage cache - 📦 Version Lock: Use
pnpm-lock.yaml
in git - 🚀 Script Filters: Use
--filter
for targeted operations
🧪 Hands-On Exercise
🎯 Challenge: Build a TypeScript Microservices Setup
Create a production-ready TypeScript monorepo with microservices:
📋 Requirements:
- 🏗️ Multiple service packages (auth, api, frontend)
- 📦 Shared utilities package
- 🧪 Unified testing setup
- 🚀 Build optimization with caching
- 🔒 TypeScript strict mode
- 🎨 Each service needs unique emoji branding!
🚀 Bonus Points:
- Add Docker support for each service
- Implement inter-service type sharing
- Set up automated dependency updates
- Create deployment scripts
💡 Solution
🔍 Click to see solution
# 🏗️ pnpm-workspace.yaml
packages:
- 'services/*'
- 'packages/*'
- 'apps/*'
// 📦 Root package.json
{
"name": "typescript-microservices",
"private": true,
"scripts": {
"build": "pnpm -r build",
"dev": "pnpm -r --parallel dev",
"test": "pnpm -r test",
"lint": "pnpm -r --parallel lint",
"clean": "pnpm -r exec -- rm -rf dist",
"deploy": "pnpm build && pnpm --filter './services/*' deploy"
},
"devDependencies": {
"typescript": "^5.0.0",
"jest": "^29.0.0",
"@types/jest": "^29.0.0",
"eslint": "^8.36.0"
}
}
// 📦 packages/shared/src/types.ts
export interface ServiceResponse<T = any> {
success: boolean;
data?: T;
error?: string;
emoji: string; // 🎨 Service branding
}
export interface User {
id: string;
email: string;
name: string;
avatar?: string; // 👤 User avatar emoji
}
export interface AuthToken {
token: string;
expires: Date;
userId: string;
}
// 🎯 Type-safe service configuration
export interface ServiceConfig {
name: string;
port: number;
emoji: string;
version: string;
dependencies: string[];
}
// 🔐 services/auth/src/auth.ts
import { ServiceResponse, User, AuthToken } from '@shared/types';
export class AuthService {
private config: ServiceConfig = {
name: 'auth-service',
port: 3001,
emoji: '🔐',
version: '1.0.0',
dependencies: ['@shared/types']
};
// 🎯 Login user
async login(email: string, password: string): Promise<ServiceResponse<AuthToken>> {
try {
// 🔍 Validate credentials
const user = await this.validateUser(email, password);
if (!user) {
return {
success: false,
error: '❌ Invalid credentials',
emoji: this.config.emoji
};
}
// 🎫 Generate token
const token: AuthToken = {
token: this.generateJWT(user.id),
expires: new Date(Date.now() + 24 * 60 * 60 * 1000), // 24h
userId: user.id
};
return {
success: true,
data: token,
emoji: this.config.emoji
};
} catch (error) {
return {
success: false,
error: '💥 Authentication failed',
emoji: this.config.emoji
};
}
}
private async validateUser(email: string, password: string): Promise<User | null> {
// 🔍 Database lookup logic
console.log(`🔐 Validating user: ${email}`);
return null; // Placeholder
}
private generateJWT(userId: string): string {
// 🎫 JWT generation logic
return `jwt-token-${userId}-${Date.now()}`;
}
}
# 🚀 pnpm commands for the project
# Install all dependencies
pnpm install
# Add shared dependency to auth service
pnpm add @shared/types --filter auth-service
# Run specific service in development
pnpm --filter auth-service dev
# Build all services
pnpm -r build
# Test everything
pnpm -r test
# Deploy all services
pnpm run deploy
# 🐳 services/auth/Dockerfile
FROM node:18-alpine
WORKDIR /app
# 📦 Copy package files
COPY package.json pnpm-lock.yaml ./
COPY packages/shared ./packages/shared
COPY services/auth ./services/auth
# 🚀 Install pnpm and dependencies
RUN npm install -g pnpm
RUN pnpm install --frozen-lockfile
# 🏗️ Build the service
RUN pnpm --filter auth-service build
# 🎯 Start the service
EXPOSE 3001
CMD ["pnpm", "--filter", "auth-service", "start"]
🎓 Key Takeaways
You’ve learned so much! Here’s what you can now do:
- ✅ Setup pnpm in any TypeScript project 💪
- ✅ Manage dependencies efficiently and safely 🛡️
- ✅ Create monorepos with workspace management 🎯
- ✅ Debug pnpm issues like a pro 🐛
- ✅ Optimize builds with advanced configurations 🚀
Remember: pnpm isn’t just faster—it’s also safer and more predictable! It helps you build better TypeScript applications. 🤝
🤝 Next Steps
Congratulations! 🎉 You’ve mastered pnpm package management!
Here’s what to do next:
- 💻 Practice by converting an existing npm project to pnpm
- 🏗️ Build a monorepo using pnpm workspaces
- 📚 Move on to our next tutorial: Advanced Build Optimization
- 🌟 Share your pnpm success stories with the community!
Remember: Every TypeScript expert started with learning the tools. Keep building, keep optimizing, and most importantly, have fun with your lightning-fast installs! ⚡
Happy coding! 🎉🚀✨