Hone logo
Hone
Problems

Type-Level Module Resolution in TypeScript

This challenge focuses on building a system at the TypeScript type level that simulates module resolution. You will create a type that, given a string representing a module path, can infer the "contents" of that module based on a predefined structure of available modules. This is a fascinating exploration of TypeScript's type system for static analysis and meta-programming.

Problem Description

Your task is to design and implement a TypeScript type, let's call it ResolveModule<Path extends string, Modules>, that takes two type arguments:

  1. Path: A string literal representing the module path to resolve (e.g., "./utils/math").
  2. Modules: A structure representing your available modules and their contents.

The ResolveModule type should:

  • Locate the specified module: Given a Path, it should find the corresponding module within the Modules structure.
  • Return module contents: If the module is found, it should return a representation of its contents. The specific representation of "contents" will be defined in the Modules structure.
  • Handle nested modules: The Path can represent nested modules (e.g., utils/math or ./utils/math).
  • Handle relative paths: Support for basic relative path resolution (e.g., . for the current directory, .. for the parent directory) should be considered.
  • Handle absent modules: If a module path does not exist within the Modules structure, the type should indicate this, perhaps by returning never or a specific error type.

For simplicity, assume the Modules structure is a nested object-like type where keys represent module names (or parts of module paths) and values are either further nested module structures or the actual "content" of a module.

Examples

Example 1: Simple Module Resolution

// Assume this is your module registry
type MyModules = {
  utils: {
    math: {
      add: "function add(a: number, b: number): number";
    };
    strings: {
      capitalize: "function capitalize(str: string): string";
    };
  };
  app: {
    main: "class Main {}";
  };
};

// Resolving a direct module
type ResolvedAdd = ResolveModule<"./utils/math/add", MyModules>;
// Expected Output: "function add(a: number, b: number): number"

// Resolving a directory
type ResolvedUtilsMath = ResolveModule<"./utils/math", MyModules>;
// Expected Output: { add: "function add(a: number, b: number): number"; }

// Resolving a top-level module
type ResolvedAppMain = ResolveModule<"app/main", MyModules>;
// Expected Output: "class Main {}"

Explanation: The ResolveModule type navigates the MyModules structure based on the segments in the path string. For "./utils/math/add", it traverses utils -> math -> add to retrieve its string representation. For "./utils/math", it returns the object representing the math module's exports.

Example 2: Handling Relative Paths and Non-existent Modules

type MyModules = {
  core: {
    api: {
      index: "interface API {}";
    };
  };
};

// Resolving with '.'
type ResolvedCoreApi = ResolveModule<"./core/api", MyModules>;
// Expected Output: { index: "interface API {}"; }

// Resolving with '../' (assuming the "current" is implicitly the root of MyModules)
// This example is tricky and might require defining a base context.
// For this challenge, let's assume '.' refers to the *root* of the provided Modules object.
// So, if we were resolving from within 'core', './' would be 'core'.
// Let's define a slightly different structure to better illustrate relative paths.

type ProjectModules = {
  src: {
    components: {
      button: "type ButtonProps = { ... }";
      input: "type InputProps = { ... }";
    };
    utils: {
      helpers: {
        format: "function formatValue(val: any): string";
      };
    };
  };
};

// Assume we are "conceptually" at './src/components' for path resolution
// This is an advanced consideration. For the core challenge, let's stick to paths
// that resolve directly from the root of the Modules object for now, and clarify
// how relative paths are handled in the Notes.

// Let's re-evaluate relative paths:
// For simplicity in this challenge, let's assume the `Path` *always* starts from the root
// of the `Modules` object provided. So, `.` is redundant and `..` is not applicable
// unless we introduce a concept of a "current module" type, which is outside the scope
// of this initial problem.

// Revised Example 2: Non-existent Module
type ResolvedNonExistent = ResolveModule<"./nonexistent/module", MyModules>;
// Expected Output: never (or a specific error type like "ModuleNotFound")

Explanation: If the path segments do not lead to a valid module within the MyModules structure, the ResolveModule type should return never to indicate an error at the type level.

Constraints

  • The Path will be a string literal type.
  • The Modules structure will be a recursively defined object-like type (similar to JSON objects) where keys are string literals representing module names/paths and values are either nested structures or string literals representing module content.
  • You do not need to implement full ./ and ../ relative path resolution. For this challenge, assume the Path is resolved directly from the root of the Modules object. The leading ./ should be treated as an implicit root indicator.
  • The maximum depth of module nesting is not strictly limited, but assume it's reasonable for typical codebases (e.g., less than 10 levels deep).
  • The number of modules is not strictly limited.

Notes

  • This challenge is about manipulating string literals and object-like types within TypeScript's conditional types and template literal types.
  • You'll likely need to use string manipulation types (like Split, First, Rest, Join) to break down the Path string into segments. A helper type Split<S, D> that splits a string S by delimiter D into an array of string literals would be very useful.
  • Consider how to represent "module content". String literals are used in the examples, but you could also imagine returning object types representing exported members.
  • Think about how to handle the case where a path segment exists as a key, but the value is not a further module structure (e.g., trying to access utils/math/add/something when add is a string literal).
  • For error handling, returning never is a common TypeScript pattern for indicating an invalid type. You could also define a custom type ModuleNotFound = "Module not found"; and return that.
  • The focus is on static analysis. The ResolveModule type will be evaluated entirely at compile time by the TypeScript compiler.
Loading editor...
typescript