Hone logo
Hone
Problems

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:

  1. Shared Type Definition: Create a mechanism to define types that can be exported from a designated "shared" package.
  2. Package Structure Simulation: Simulate a monorepo structure with at least two packages: a shared package and one or more application packages.
  3. Type Consumption: Application packages should be able to import and use types defined in the shared package.
  4. 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.
  5. Inter-package Dependencies: Demonstrate how these dependencies would be declared and managed (conceptually, not a full npm install simulation).
  6. 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.json in the root of your monorepo to facilitate type checking and compilation across packages. Specifically, consider paths or other mechanisms to help TypeScript resolve imports like import { 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.json file 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.
Loading editor...
typescript