Hone logo
Hone
Problems

Building a Gradual Typing System in TypeScript

This challenge asks you to implement a simplified, user-defined gradual typing system within TypeScript. The goal is to allow developers to define custom type annotations that can be checked at runtime, providing an additional layer of validation beyond static TypeScript checks. This is useful for scenarios where runtime type safety is critical, especially when dealing with external data or complex application logic.

Problem Description

Your task is to create a system that allows users to define "type guards" and apply them to variables or function arguments. These type guards will be functions that return a boolean, indicating whether a given value conforms to the desired type. The system should also include a mechanism to enforce these type guards at runtime.

Key Requirements:

  1. Define Custom Type Guards: Users should be able to define functions that act as type guards. These functions will take a value and return true if the value is of the expected type, and false otherwise.
  2. Runtime Enforcement: Implement a mechanism to automatically run these type guards when a value is assigned or passed to a function.
  3. Error Handling: If a type guard fails at runtime, throw a descriptive error indicating the variable name (if applicable) and the expected type.
  4. Integration with TypeScript: The system should ideally be able to leverage TypeScript's static typing for static analysis, while adding runtime checks.

Expected Behavior:

  • Successful Type Check: If a value passes its defined type guard, the program should continue executing without interruption.
  • Failed Type Check: If a value fails its type guard, an error should be thrown.

Edge Cases to Consider:

  • null and undefined values.
  • Nested data structures (e.g., objects with properties that also have type guards).
  • Functions with multiple arguments, each potentially having a type guard.

Examples

Example 1:

Input:

// Define a type guard for positive numbers
function isPositiveNumber(value: any): value is number {
    return typeof value === 'number' && value > 0;
}

// Apply the type guard at runtime
let myPositiveNumber: number & typeof isPositiveNumber = 10; // Static type is number, runtime check for positive

console.log(myPositiveNumber); // Output: 10

try {
    let invalidNumber: number & typeof isPositiveNumber = -5;
} catch (e: any) {
    console.error(e.message); // Expected output: Runtime type error for 'invalidNumber': Expected to be a positive number.
}

Output:

10
Runtime type error for 'invalidNumber': Expected to be a positive number.

Explanation: The first assignment is successful because 10 is a positive number. The second assignment attempts to assign -5, which fails the isPositiveNumber type guard, triggering a runtime error.

Example 2:

Input:

interface User {
    name: string;
    age: number;
}

function isUser(value: any): value is User {
    return typeof value === 'object' && value !== null &&
           typeof value.name === 'string' && typeof value.age === 'number';
}

function processUser(userData: User & typeof isUser) {
    console.log(`Processing user: ${userData.name}, Age: ${userData.age}`);
}

let validUser = { name: "Alice", age: 30 };
processUser(validUser as User & typeof isUser); // Type assertion to satisfy static analysis

let invalidUser = { name: "Bob" };
try {
    processUser(invalidUser as User & typeof isUser);
} catch (e: any) {
    console.error(e.message); // Expected output: Runtime type error for 'userData': Expected to be a User.
}

Output:

Processing user: Alice, Age: 30
Runtime type error for 'userData': Expected to be a User.

Explanation: The processUser function is designed to accept a User object that also satisfies the isUser type guard. The first call with validUser succeeds. The second call with invalidUser throws a runtime error because invalidUser lacks the age property, failing the isUser check.

Example 3:

Input:

function isStringArray(value: any): value is string[] {
    if (!Array.isArray(value)) {
        return false;
    }
    return value.every(item => typeof item === 'string');
}

let myStringArray: string[] & typeof isStringArray = ["hello", "world"]; // Static type is string[], runtime check for string array

console.log(myStringArray);

try {
    let mixedArray: string[] & typeof isStringArray = ["hello", 123];
} catch (e: any) {
    console.error(e.message); // Expected output: Runtime type error for 'mixedArray': Expected to be a string array.
}

Output:

[ 'hello', 'world' ]
Runtime type error for 'mixedArray': Expected to be a string array.

Explanation: The first assignment is valid. The second assignment attempts to create an array with a number, which violates the isStringArray type guard, resulting in a runtime error.

Constraints

  • The type guard functions must be pure functions (no side effects).
  • The system should not significantly impact runtime performance for well-typed code.
  • The solution should be written in TypeScript.
  • The error messages should be informative and include the name of the variable or parameter that failed the check and a description of the expected type (derived from the type guard's intent).

Notes

Consider how you might create a mechanism to "decorate" or wrap values or functions to automatically apply these runtime checks. You might need to explore TypeScript's type system, particularly intersection types and type assertions, to achieve the desired integration. Think about how to capture the intended name of the type for error messages.

Loading editor...
typescript