TypeScript
Completed
-
Section 1: Getting Started
-
Section 2: Basic Types
-
Section 3: Functions and Objects
-
Section 4: Advanced Types
-
Section 5: Object-Oriented TypeScript
-
Section 6: Working with Modules
-
Section 7: Tooling and Practice
-
Section 8: Type-Level Programming
-
Section 9: TypeScript with Backends
-
Section 10: Testing Typed Code
-
Section 11: Data Validation with Types
-
Section 12: Practical Projects
-
Section 13: Compiler Internals
-
Section 14: Configuration Deep Dive
-
Section 15: Enums, Symbols, and Special Types
-
Section 16: Working with Async Code
-
Section 17: TypeScript and the DOM
-
Section 18: Advanced Generics Practice
-
Section 19: Working with Third-Party Types
-
Section 20: Monorepo and Large-Scale Practices
-
Section 21: Common Pitfalls and Best Practices
-
Section 22: Interview Practice
-
Section 23: Handbook: Narrowing In Depth
-
Section 24: Handbook: Object Types In Depth
-
Section 25: Handbook: Classes In Depth
-
Section 26: Handbook: Modules In Depth
-
Section 27: Handbook: Declaration Files In Depth
-
Section 28: JSX and Namespaces
-
Section 29: Compiler Configuration Reference
-
Section 30: More Practice Exercises
-
Section 31: Handbook: Everyday Types Deep Dive
-
Section 32: Utility Types Full Reference
-
Section 33: Decorators Reference
-
Section 34: Mixins and Advanced OOP Patterns
-
Section 35: Iterators and Generators Typing
-
Section 36: More Type-Level Programming Practice
-
Section 37: TypeScript Ecosystem Tools
-
Section 38: TypeScript with Testing Frameworks
-
Section 39: TypeScript for Library Authors
-
Section 40: More Interview Practice
-
Section 41: Handbook: Functions In Depth
-
Section 42: Handbook: Type Manipulation Deep Dive
-
Section 43: More Real-World Patterns
-
Section 44: TypeScript Release Notes Highlights
-
Section 45: Final Practice and Review
118: Shared Type Packages Across a Monorepo
Imagine you're overseeing the construction of a high-end custom home. You've got an electrician and a plumber working on the same house. Now, the electrician doesn't need to know how to solder a pipe, and the plumber doesn't care about circuit breakers. However, they both absolutely need to be looking at the exact same architectural blueprint. If the plumber decides the main water line goes through the north wall, but the electrician's version of the blueprint says that's where the main breaker panel goes, you're going to have a very expensive, very wet disaster on your hands.
In a TypeScript monorepo, your "workers" are your different applications—maybe a Next.js frontend and a NestJS backend. The "blueprint" is your set of shared types. If you define a User interface in the backend and then manually recreate a similar User interface in the frontend, you've just handed your workers two different blueprints. The moment you add a middleName field to the backend but forget to update the frontend, your app will likely crash or behave unpredictably the second that data hits the browser.
The Shared Types Package Architecture
Instead of duplicating interfaces, we create a dedicated package—usually something like @my-project/types—that lives inside your packages/ folder. Both your frontend and backend then list this package as a dependency. This ensures there is one single source of truth.
Let's look at a real scenario: an e-commerce order system. You don't want to guess if the orderStatus is a string or a specific union of literals in both apps.
// packages/types/src/index.ts
export type OrderStatus = 'pending' | 'shipped' | 'delivered' | 'cancelled';
export interface Order {
id: string;
customerId: string;
items: Array<{ productId: string; quantity: number }>;
total: number;
status: OrderStatus;
createdAt: string;
}
export interface CreateOrderRequest {
customerId: string;
items: Array<{ productId: string; quantity: number }>;
}
By putting this in its own package, you aren't just sharing code; you're sharing a contract. I've spent far too many late nights debugging "undefined" errors only to realize the backend was sending order_id (snake_case) while the frontend was expecting orderId (camelCase). A shared package kills that class of bug entirely.
Wiring the Monorepo Workspace
To make this work, you can't just use relative imports like ../../packages/types. That's a recipe for madness once your folder structure grows. You need to use your package manager's workspace feature (npm, pnpm, or Yarn) to treat your types folder as a local node module.
Your packages/types/package.json should look something like this:
{
"name": "@my-app/types",
"version": "1.0.0",
"main": "./src/index.ts",
"types": "./src/index.ts"
}
Now, in your apps/api/package.json and apps/web/package.json, you simply add "@my-app/types": "workspace:*" (if using pnpm) or the equivalent for your manager. Now, in your actual logic, you import it like any other library:
// apps/api/src/orders.service.ts
import { Order, OrderStatus } from '@my-app/types';
async function updateOrder(id: string, status: OrderStatus): Promise<Order> {
// The compiler now enforces that 'status' is one of our allowed literals
// and that the returned object matches the Order interface exactly.
}
Avoiding the Circular Dependency Trap
Here is a bit of hard-earned advice: keep your shared types package pure. I've seen developers try to put "helper functions" or "validation logic" in the shared types package. While it seems convenient, this often leads to a circular dependency nightmare where your types package suddenly depends on a library that depends on your API.
If you need shared logic (like a Zod schema for validation), I recommend creating a separate @my-app/shared-utils package. Keep the @my-app/types package focused strictly on TypeScript definitions. It should have zero production dependencies. This keeps your build times fast and your dependency graph clean.
📋 Practical Task
Synchronizing the Payment Gateway Interface
You are working on a monorepo for a subscription service. Currently, the PaymentRequest type is duplicated in both the apps/client and apps/server folders, and they have drifted apart. The server expects currencyCode, but the client is sending currency.
Your Task:
- Create a new shared package directory
packages/shared-types. - Define a
PaymentRequestinterface inpackages/shared-types/index.tsthat includes:amount(number)currencyCode(string)paymentMethodId(string)description(optional string)
- Update the
apps/server/payment.controller.tsto importPaymentRequestfrom@my-app/shared-typesinstead of using a local definition. - Update the
apps/client/checkout-form.tsto import the samePaymentRequestand fix the property name fromcurrencytocurrencyCodeso the TypeScript compiler is satisfied.
There are no comments for now.