Implementing a Monorepo Type System with TypeScript
This challenge focuses on building a foundational type system for a monorepo in TypeScript. A monorepo is a single repository that contains multiple distinct projects. Managing shared types across these projects efficiently is crucial for maintainability, consistency, and developer productivity. You will create a system that allows defining, sharing, and consuming types across different packages within a simulated monorepo structure.
Problem Description
Your task is to design and implement a TypeScript type system that facilitates sharing common types across multiple packages within a monorepo. This system should enable defining types in a "shared" package and then making them available and type-safe for consumption in other "application" packages. You will need to simulate the monorepo structure and ensure that TypeScript's type checking correctly infers and validates the usage of these shared types.
Key Requirements:
- Shared Type Definition: Create a mechanism to define types that can be exported from a designated "shared" package.
- Package Structure Simulation: Simulate a monorepo structure with at least two packages: a
sharedpackage and one or moreapplicationpackages. - Type Consumption: Application packages should be able to import and use types defined in the
sharedpackage. - Type Safety: TypeScript's compiler should enforce type safety when shared types are used in application packages, including checking for correct properties, types, and values.
- Inter-package Dependencies: Demonstrate how these dependencies would be declared and managed (conceptually, not a full
npm installsimulation). - Build/Compilation: The solution should be compilable and executable with
tsc(TypeScript compiler), simulating a build process.
Expected Behavior:
When types are defined in shared/index.ts and imported into app1/index.ts, operations using these imported types in app1/index.ts should pass TypeScript compilation if they adhere to the shared type definitions. If an operation violates the shared type contract, TypeScript should report a compilation error.
Edge Cases to Consider:
- Circular Dependencies: While this challenge doesn't explicitly require solving complex circular dependency issues, be mindful of how your structure might contribute to or avoid them.
- Type Re-exporting: How would you handle re-exporting types from a shared package to further structure your monorepo?
- Version Collisions (Conceptual): Understand that in a real monorepo, managing different versions of shared packages is a concern, though you won't be implementing a full package manager.
Examples
Let's consider a scenario where we have a User type that needs to be shared.
Example 1: Basic Type Sharing
-
shared/index.ts:export interface User { id: number; username: string; email?: string; } -
app1/index.ts:import { User } from 'shared'; // Assuming 'shared' is a recognized package name function displayUser(user: User) { console.log(`User ID: ${user.id}, Username: ${user.username}`); if (user.email) { console.log(`Email: ${user.email}`); } } const myUser: User = { id: 1, username: "alice", email: "alice@example.com" }; displayUser(myUser); -
app2/index.ts:import { User } from 'shared'; function processUser(user: User) { // Placeholder for some processing console.log(`Processing user: ${user.username}`); // For demonstration, let's try to access a non-existent property // console.log(user.age); // This should cause a TypeScript error } const anotherUser: User = { id: 2, username: "bob" }; processUser(anotherUser);
Expected Output (Conceptual - Successful Compilation):
The TypeScript compiler (tsc) should successfully compile all .ts files without errors, indicating that app1/index.ts and app2/index.ts correctly import and use the User type from the shared package. If the commented-out console.log(user.age) in app2/index.ts were uncommented, tsc would report a type error.
Example 2: Invalid Type Usage (Illustrating Type Safety)
Consider the same shared/index.ts as above.
app1/index.ts(modified):import { User } from 'shared'; // Attempting to use a property of the wrong type const invalidUser: User = { id: "not a number", // Error: Type 'string' is not assignable to type 'number'. username: "charlie" }; // Attempting to use a non-existent property const userWithExtraProp = { id: 3, username: "dave", age: 30 // Error: Object literal may only specify known properties, and 'age' does not exist in type 'User'. }; // Function that expects User but receives something else function greetUser(user: User) { console.log(`Hello, ${user.username}`); } // greetUser({ name: "eve" }); // Error: Argument of type '{ name: string; }' is not assignable to parameter of type 'User'.
Expected Output (Conceptual - Compilation Errors):
When tsc is run, it should immediately report type errors for the invalid assignments and function calls, demonstrating that the type system correctly prevents incorrect usage of the shared User type.
Constraints
- File Structure: You are to simulate a directory structure like:
/monorepo-root /packages /shared index.ts /app1 index.ts /app2 index.ts tsconfig.json - TypeScript Version: Use a recent, stable version of TypeScript (e.g., 4.x or 5.x).
- No External Libraries for Type Management: Focus on core TypeScript features and standard library types. Do not use external monorepo management tools like Lerna or Nx for this challenge, but understand how your solution conceptually fits into such an ecosystem.
- Compilation: The solution must be compilable using
tsc.
Notes
- Think about how you would configure
tsconfig.jsonin the root of your monorepo to facilitate type checking and compilation across packages. Specifically, considerpathsor other mechanisms to help TypeScript resolve imports likeimport { User } from 'shared';. - The goal is to demonstrate a type system, not a full build pipeline. Focus on the TypeScript code and its compilation.
- Your solution should be presented as a set of TypeScript files and a
tsconfig.jsonfile that, when compiled, proves the type safety of your monorepo structure. - Consider how you might extend this to include interfaces, classes, enums, and functions that are shared across packages.