Hone logo
Hone
Problems

Implement Type-Safe Assertion Functions in TypeScript

Developing robust applications often involves validating assumptions about your data. In TypeScript, static type checking helps prevent many errors at compile time, but runtime assertions are crucial for catching unexpected states or invalid inputs that might slip through. This challenge asks you to build a set of flexible and type-safe assertion functions that can be used to enforce conditions on your variables at runtime.

Problem Description

Your task is to create a set of TypeScript functions that act as runtime assertions. These functions will take a value and a condition, and if the condition is not met, they will throw an error. The key requirement is that these functions should be type-safe, meaning they should leverage TypeScript's type system to infer and preserve the type of the value being asserted.

Key Requirements:

  1. assert(value: T, message?: string): asserts value is T: This is the base assertion function. It should return true if the value is truthy and throw an Error with an optional message otherwise. Crucially, it must use a TypeScript asserts clause to narrow down the type of value to its original type T in the scope after the assertion.

  2. assertEquals<T>(actual: T, expected: T, message?: string): asserts actual is T: This function asserts that actual is strictly equal (===) to expected. If they are not equal, it throws an Error with an optional message. It should also use an asserts clause.

  3. assertIsString(value: any, message?: string): asserts value is string: This function asserts that the given value is a string. If not, it throws an Error with an optional message.

  4. assertIsNumber(value: any, message?: string): asserts value is number: This function asserts that the given value is a number. If not, it throws an Error with an optional message.

  5. assertIsBoolean(value: any, message?: string): asserts value is boolean: This function asserts that the given value is a boolean. If not, it throws an Error with an optional message.

  6. assertIsObject(value: any, message?: string): asserts value is object: This function asserts that the given value is an object (and not null). If not, it throws an Error with an optional message.

  7. assertIsArray(value: any, message?: string): asserts value is Array<any>: This function asserts that the given value is an array. If not, it throws an Error with an optional message.

Expected Behavior:

  • If the assertion condition is met, the function should return normally, and TypeScript should understand that the value being asserted now conforms to the asserted type (due to the asserts clause).
  • If the assertion condition is not met, an Error object should be thrown.
  • The error message should be informative, ideally including the provided message or a default one.

Edge Cases to Consider:

  • null and undefined values.
  • The assert function specifically: what constitutes a "truthy" value? (Standard JavaScript truthiness rules apply).
  • The assertIsObject function: typeof null returns "object", so you need to handle null explicitly.

Examples

Example 1: Basic Assertion

function processData(data: string | undefined) {
  assert(data, "Data cannot be undefined or empty.");
  // After this line, TypeScript knows 'data' is definitely a string.
  console.log(data.toUpperCase()); // No TypeScript error here
}

// Test cases:
// processData("hello"); // Output: HELLO
// processData(undefined); // Throws: Error: Data cannot be undefined or empty.
// processData(""); // Throws: Error: Data cannot be undefined or empty.

Example 2: Equality Assertion

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

function getUserById(users: User[], id: number): User | undefined {
  const user = users.find(u => u.id === id);
  // Assume we expect a user to be found for valid IDs
  const foundUser = assertEquals(user, undefined, `User with ID ${id} not found.`);
  // This line won't be reached if user is undefined.
  // If it is reached, it means user was NOT undefined, but this syntax is illustrative of expectation.
  // The actual type guarding happens because 'user' would remain 'User | undefined'
  // until 'assertEquals' successfully returns.
  // For this example, let's imagine a scenario where the return type indicates the type guard.

  // A more direct example demonstrating type narrowing from equality:
  let status: "pending" | "completed" | "failed" = "pending";
  assertEquals(status, "completed", "Operation should be completed.");
  // If this assertion passes, TypeScript knows 'status' is 'completed' here.
  console.log(status); // TS knows 'status' is 'completed'
}

// Test cases:
// const usersData = [{ id: 1, name: "Alice" }, { id: 2, name: "Bob" }];
// getUserById(usersData, 1); // No error, but the example needs adjustment for clarity.
// Let's refine Example 2's explanation for clarity.

// Refined Example 2: Demonstrating Type Narrowing with Equality
function validateStatus(currentStatus: "pending" | "completed" | "failed"): void {
  assertEquals(currentStatus, "completed", `Expected status to be 'completed', but got '${currentStatus}'.`);
  // After this assertion, TypeScript knows 'currentStatus' must be 'completed' if no error was thrown.
  console.log("Status is confirmed completed.");
}

// Test cases:
// validateStatus("completed"); // Output: Status is confirmed completed.
// validateStatus("pending"); // Throws: Error: Expected status to be 'completed', but got 'pending'.

Example 3: Type Specific Assertion

function processConfig(config: any): void {
  assertIsString(config.url, "Configuration URL must be a string.");
  assertIsNumber(config.timeout, "Configuration timeout must be a number.");
  assertIsObject(config.headers, "Configuration headers must be an object.");
  assertIsArray(config.plugins, "Configuration plugins must be an array.");

  // After these assertions, TypeScript knows the types:
  const url: string = config.url;
  const timeout: number = config.timeout;
  const headers: object = config.headers;
  const plugins: Array<any> = config.plugins;

  console.log(`Processing URL: ${url} with timeout ${timeout}`);
}

// Test cases:
// const validConfig = { url: "http://api.com", timeout: 5000, headers: {}, plugins: [] };
// processConfig(validConfig); // Outputs: Processing URL: http://api.com with timeout 5000

// const invalidConfig = { url: 123, timeout: "fast", headers: null, plugins: {} };
// processConfig(invalidConfig); // Throws: Error: Configuration URL must be a string.

Constraints

  • All implemented assertion functions must be written in TypeScript.
  • Functions should accept an optional message string for custom error reporting.
  • The asserts type predicate mechanism in TypeScript must be used for type narrowing.
  • Do not use any external assertion libraries.
  • Focus on correctness and type safety rather than extreme performance optimization.

Notes

  • The asserts keyword is crucial here. It allows you to tell the TypeScript compiler that if the function doesn't throw an error, the type of the asserted variable is narrowed down. For example, asserts value is string means if assertIsString completes without throwing, value can be treated as a string in the subsequent code.
  • Pay close attention to the typeof operator's behavior, especially with null.
  • Consider how to construct informative default error messages when no custom message is provided.
  • For assertIsObject, remember that null also has typeof null === 'object'. You'll need an additional check for null.
Loading editor...
typescript