Prerequisites
- Basic understanding of JavaScript 📝
- TypeScript installation ⚡
- VS Code or preferred IDE 💻
- Understanding of npm/pnpm workspaces 📦
What you'll learn
- Understand project references fundamentals 🎯
- Apply project references in real projects 🏗️
- Debug common project references issues 🐛
- Write type-safe multi-package code ✨
🎯 Introduction
Welcome to this exciting tutorial on TypeScript Project References! 🎉 In this guide, we’ll explore how to build powerful multi-package TypeScript projects that scale like a dream.
You’ll discover how project references can transform your monorepo development experience. Whether you’re building a component library 📚, microservices architecture 🌐, or complex applications with shared utilities 🛠️, understanding project references is essential for maintaining clean, fast, and reliable builds.
By the end of this tutorial, you’ll feel confident organizing large TypeScript codebases with project references! Let’s dive in! 🏊♂️
📚 Understanding Project References
🤔 What are Project References?
Project references are like organizing a big company into departments 🏢. Think of it as creating a clear hierarchy where each department (package) knows exactly which other departments it depends on, and TypeScript can build them in the perfect order!
In TypeScript terms, project references allow you to:
- ✨ Break large projects into smaller, manageable pieces
- 🚀 Build only what changed with incremental compilation
- 🛡️ Enforce proper dependencies between packages
- 📦 Share types and code safely across packages
💡 Why Use Project References?
Here’s why developers love project references:
- Faster Builds ⚡: Build only what changed, not everything
- Better Organization 📋: Clear separation of concerns
- Type Safety 🔒: Shared types across packages
- Dependency Management 🎯: Prevent circular dependencies
Real-world example: Imagine building an e-commerce platform 🛒. With project references, you can have separate packages for authentication, payment processing, and UI components, all working together seamlessly!
🔧 Basic Syntax and Usage
📝 Simple Project Structure
Let’s start with a friendly example:
my-monorepo/
├── packages/
│ ├── shared/ # 📚 Shared utilities
│ ├── ui/ # 🎨 UI components
│ └── app/ # 🚀 Main application
├── tsconfig.json # 🎯 Root config
└── package.json # 📦 Root package
🎯 Basic Configuration
Here’s how to set up project references:
// 👋 Root tsconfig.json
{
"compilerOptions": {
"target": "ES2020",
"module": "commonjs",
"strict": true,
"composite": true, // 🔑 Enable for references
"declaration": true, // 📝 Generate .d.ts files
"declarationMap": true // 🗺️ Source maps for declarations
},
"references": [
{ "path": "./packages/shared" }, // 📚 Shared package
{ "path": "./packages/ui" }, // 🎨 UI package
{ "path": "./packages/app" } // 🚀 App package
],
"files": [] // 🎯 Root doesn't compile files directly
}
💡 Explanation: The composite
flag tells TypeScript this project can be referenced by others. The references
array defines the build order!
💡 Practical Examples
🛒 Example 1: E-Commerce Monorepo
Let’s build a real multi-package setup:
// 📚 packages/shared/src/types.ts
export interface Product {
id: string;
name: string;
price: number;
emoji: string; // Every product needs personality! 🎨
}
export interface User {
id: string;
name: string;
email: string;
favoriteEmoji: string; // 😊 User's favorite emoji
}
export interface Order {
id: string;
userId: string;
products: Product[];
total: number;
status: "pending" | "processing" | "shipped" | "delivered";
trackingEmoji: string; // 📦 Visual status
}
// 📚 packages/shared/tsconfig.json
{
"compilerOptions": {
"target": "ES2020",
"module": "commonjs",
"strict": true,
"composite": true,
"declaration": true,
"declarationMap": true,
"outDir": "dist",
"rootDir": "src"
},
"include": ["src/**/*"]
}
// 🎨 packages/ui/src/ProductCard.tsx
import { Product } from '@shared/types'; // 🎯 Import from shared package
export interface ProductCardProps {
product: Product;
onAddToCart: (product: Product) => void;
}
export const ProductCard: React.FC<ProductCardProps> = ({ product, onAddToCart }) => {
// 🎨 Beautiful product card component
return (
<div className="product-card">
<span className="product-emoji">{product.emoji}</span>
<h3>{product.name}</h3>
<p className="price">${product.price}</p>
<button onClick={() => onAddToCart(product)}>
Add to Cart 🛒
</button>
</div>
);
};
// 🎨 packages/ui/tsconfig.json
{
"compilerOptions": {
"target": "ES2020",
"module": "commonjs",
"strict": true,
"composite": true,
"declaration": true,
"declarationMap": true,
"outDir": "dist",
"rootDir": "src",
"jsx": "react"
},
"references": [
{ "path": "../shared" } // 🔗 Depends on shared package
],
"include": ["src/**/*"]
}
🎯 Try it yourself: Add a UserProfile
component that uses the User
type from shared!
🎮 Example 2: Gaming Platform
Let’s make it more exciting:
// 📚 packages/game-engine/src/types.ts
export interface GamePlayer {
id: string;
username: string;
level: number;
experience: number;
avatar: string; // 🎮 Player avatar emoji
}
export interface GameSession {
id: string;
players: GamePlayer[];
startTime: Date;
status: "waiting" | "playing" | "finished";
gameType: "puzzle" | "action" | "strategy";
emoji: string; // 🎯 Game type emoji
}
export class GameEngine {
private sessions: Map<string, GameSession> = new Map();
// 🚀 Create new game session
createSession(gameType: GameSession['gameType']): GameSession {
const session: GameSession = {
id: Date.now().toString(),
players: [],
startTime: new Date(),
status: "waiting",
gameType,
emoji: this.getGameEmoji(gameType)
};
this.sessions.set(session.id, session);
console.log(`🎮 Created ${session.emoji} ${gameType} session!`);
return session;
}
// 🎨 Get emoji for game type
private getGameEmoji(gameType: GameSession['gameType']): string {
const emojiMap = {
puzzle: "🧩",
action: "⚡",
strategy: "🧠"
};
return emojiMap[gameType];
}
// 👥 Add player to session
addPlayer(sessionId: string, player: GamePlayer): void {
const session = this.sessions.get(sessionId);
if (session && session.status === "waiting") {
session.players.push(player);
console.log(`✨ ${player.avatar} ${player.username} joined the game!`);
}
}
}
// 🎨 packages/game-ui/src/GameLobby.tsx
import { GameSession, GamePlayer } from '@game-engine/types';
export interface GameLobbyProps {
session: GameSession;
currentPlayer: GamePlayer;
onStartGame: () => void;
}
export const GameLobby: React.FC<GameLobbyProps> = ({
session,
currentPlayer,
onStartGame
}) => {
return (
<div className="game-lobby">
<h2>{session.emoji} {session.gameType.toUpperCase()} Game</h2>
<div className="players-list">
<h3>Players ({session.players.length})</h3>
{session.players.map(player => (
<div key={player.id} className="player-card">
<span className="avatar">{player.avatar}</span>
<span className="username">{player.username}</span>
<span className="level">Level {player.level}</span>
</div>
))}
</div>
{session.players.length >= 2 && (
<button onClick={onStartGame} className="start-button">
Start Game! 🚀
</button>
)}
</div>
);
};
🚀 Advanced Concepts
🧙♂️ Advanced Topic 1: Build Dependencies
When you’re ready to level up, try this advanced build configuration:
// 🎯 Advanced tsconfig with build dependencies
{
"compilerOptions": {
"target": "ES2020",
"module": "commonjs",
"strict": true,
"composite": true,
"declaration": true,
"declarationMap": true,
"incremental": true, // ⚡ Enable incremental compilation
"tsBuildInfoFile": ".tsbuildinfo" // 📊 Build cache file
},
"references": [
{ "path": "../shared", "prepend": false },
{ "path": "../utils", "prepend": true } // 🔗 Prepend to output
]
}
🏗️ Advanced Topic 2: Path Mapping
For the brave developers who want clean imports:
// 🗺️ Advanced path mapping
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@shared/*": ["packages/shared/src/*"], // 📚 Shared utilities
"@ui/*": ["packages/ui/src/*"], // 🎨 UI components
"@api/*": ["packages/api/src/*"], // 🔌 API layer
"@utils/*": ["packages/utils/src/*"] // 🛠️ Helper functions
}
}
}
// ✨ Clean imports everywhere!
import { User, Product } from '@shared/types';
import { Button, Card } from '@ui/components';
import { formatPrice } from '@utils/currency';
import { fetchProducts } from '@api/products';
⚠️ Common Pitfalls and Solutions
😱 Pitfall 1: Circular Dependencies
// ❌ Wrong way - creates circular dependency!
// packages/a/src/index.ts
import { funcB } from '@b/utils'; // 💥 A depends on B
// packages/b/src/index.ts
import { funcA } from '@a/utils'; // 💥 B depends on A
// ✅ Correct way - extract to shared package!
// packages/shared/src/utils.ts
export const sharedFunction = () => {
console.log("🎯 Shared logic here!");
};
// packages/a/src/index.ts
import { sharedFunction } from '@shared/utils'; // ✅ Both depend on shared
// packages/b/src/index.ts
import { sharedFunction } from '@shared/utils'; // ✅ Clean dependency
🤯 Pitfall 2: Missing Composite Flag
// ❌ Forgetting composite flag
{
"compilerOptions": {
"target": "ES2020",
"strict": true
// Missing: "composite": true 💥 References won't work!
}
}
// ✅ Always include composite for referenced projects!
{
"compilerOptions": {
"target": "ES2020",
"strict": true,
"composite": true, // 🔑 Essential for references
"declaration": true, // 📝 Also needed
"declarationMap": true // 🗺️ Helps with debugging
}
}
🛠️ Best Practices
- 🎯 Clear Dependency Flow: Always depend “upward” - apps depend on libraries, not vice versa
- 📝 Consistent Naming: Use consistent package naming conventions
- 🛡️ Enable Strict Mode: Use strict TypeScript settings across all packages
- ⚡ Use Incremental Builds: Enable incremental compilation for faster builds
- 🗺️ Path Mapping: Use path mapping for clean, readable imports
🧪 Hands-On Exercise
🎯 Challenge: Build a Social Media Platform
Create a multi-package social media platform:
📋 Requirements:
- 📚
shared
package with User, Post, and Comment types - 🎨
ui
package with social media components - 🔌
api
package with data fetching logic - 🚀
app
package that combines everything - 📱 Each post should have reaction emojis!
🚀 Bonus Points:
- Add path mapping for clean imports
- Implement incremental builds
- Create a notification system
- Add real-time features
💡 Solution
🔍 Click to see solution
// 📚 packages/shared/src/types.ts
export interface User {
id: string;
username: string;
displayName: string;
avatar: string; // 😊 User avatar emoji
bio?: string;
followers: number;
following: number;
}
export interface Post {
id: string;
userId: string;
content: string;
imageUrl?: string;
timestamp: Date;
reactions: Reaction[];
comments: Comment[];
emoji: string; // 📝 Post mood emoji
}
export interface Reaction {
userId: string;
type: "like" | "love" | "laugh" | "angry" | "sad";
emoji: string; // ❤️ Reaction emoji
timestamp: Date;
}
export interface Comment {
id: string;
userId: string;
postId: string;
content: string;
timestamp: Date;
reactions: Reaction[];
}
// 🎨 packages/ui/src/PostCard.tsx
import { Post, User, Reaction } from '@shared/types';
export interface PostCardProps {
post: Post;
author: User;
currentUser: User;
onReact: (postId: string, reaction: Reaction['type']) => void;
onComment: (postId: string, content: string) => void;
}
export const PostCard: React.FC<PostCardProps> = ({
post,
author,
currentUser,
onReact,
onComment
}) => {
const [commentText, setCommentText] = useState('');
const handleReact = (reactionType: Reaction['type']) => {
onReact(post.id, reactionType);
};
const handleComment = () => {
if (commentText.trim()) {
onComment(post.id, commentText);
setCommentText('');
}
};
return (
<div className="post-card">
<div className="post-header">
<span className="author-avatar">{author.avatar}</span>
<div className="author-info">
<h4>{author.displayName}</h4>
<span className="username">@{author.username}</span>
</div>
<span className="post-emoji">{post.emoji}</span>
</div>
<div className="post-content">
<p>{post.content}</p>
{post.imageUrl && <img src={post.imageUrl} alt="Post content" />}
</div>
<div className="post-reactions">
<button onClick={() => handleReact('like')}>👍 Like</button>
<button onClick={() => handleReact('love')}>❤️ Love</button>
<button onClick={() => handleReact('laugh')}>😂 Laugh</button>
<button onClick={() => handleReact('angry')}>😠 Angry</button>
<button onClick={() => handleReact('sad')}>😢 Sad</button>
</div>
<div className="post-comments">
<h5>💬 Comments ({post.comments.length})</h5>
{post.comments.map(comment => (
<div key={comment.id} className="comment">
<span className="comment-content">{comment.content}</span>
</div>
))}
<div className="add-comment">
<input
type="text"
value={commentText}
onChange={(e) => setCommentText(e.target.value)}
placeholder="Add a comment... 💭"
/>
<button onClick={handleComment}>Post 📝</button>
</div>
</div>
</div>
);
};
// 🔌 packages/api/src/social-api.ts
import { User, Post, Comment, Reaction } from '@shared/types';
export class SocialMediaAPI {
private baseUrl: string;
constructor(baseUrl: string) {
this.baseUrl = baseUrl;
}
// 👥 Fetch user profile
async getUser(userId: string): Promise<User> {
const response = await fetch(`${this.baseUrl}/users/${userId}`);
const user = await response.json();
console.log(`👤 Fetched user: ${user.avatar} ${user.displayName}`);
return user;
}
// 📝 Fetch posts for timeline
async getTimelinePosts(userId: string): Promise<Post[]> {
const response = await fetch(`${this.baseUrl}/users/${userId}/timeline`);
const posts = await response.json();
console.log(`📱 Fetched ${posts.length} posts for timeline`);
return posts;
}
// ❤️ React to a post
async reactToPost(postId: string, reaction: Reaction): Promise<void> {
await fetch(`${this.baseUrl}/posts/${postId}/reactions`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(reaction)
});
console.log(`${reaction.emoji} Reacted to post ${postId}!`);
}
// 💬 Add comment to post
async addComment(comment: Omit<Comment, 'id' | 'timestamp'>): Promise<Comment> {
const response = await fetch(`${this.baseUrl}/posts/${comment.postId}/comments`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(comment)
});
const newComment = await response.json();
console.log(`💬 Added comment to post ${comment.postId}!`);
return newComment;
}
}
// 🎯 Root tsconfig.json
{
"compilerOptions": {
"target": "ES2020",
"module": "commonjs",
"strict": true,
"composite": true,
"declaration": true,
"declarationMap": true,
"incremental": true,
"baseUrl": ".",
"paths": {
"@shared/*": ["packages/shared/src/*"],
"@ui/*": ["packages/ui/src/*"],
"@api/*": ["packages/api/src/*"]
}
},
"references": [
{ "path": "./packages/shared" },
{ "path": "./packages/ui" },
{ "path": "./packages/api" },
{ "path": "./packages/app" }
],
"files": []
}
🎓 Key Takeaways
You’ve learned so much! Here’s what you can now do:
- ✅ Create project references with confidence 💪
- ✅ Avoid circular dependencies that trip up beginners 🛡️
- ✅ Apply best practices in real monorepo projects 🎯
- ✅ Debug build issues like a pro 🐛
- ✅ Build scalable multi-package apps with TypeScript! 🚀
Remember: Project references are your best friend for large codebases. They help you stay organized and build faster! 🤝
🤝 Next Steps
Congratulations! 🎉 You’ve mastered TypeScript project references!
Here’s what to do next:
- 💻 Practice with the social media platform exercise
- 🏗️ Convert an existing monorepo to use project references
- 📚 Move on to our next tutorial: Advanced Build Optimization
- 🌟 Share your multi-package projects with others!
Remember: Every TypeScript expert was once building single-file projects. Keep coding, keep scaling, and most importantly, have fun building amazing things! 🚀
Happy coding! 🎉🚀✨