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:
-
assert(value: T, message?: string): asserts value is T: This is the base assertion function. It should returntrueif thevalueis truthy and throw anErrorwith an optionalmessageotherwise. Crucially, it must use a TypeScriptassertsclause to narrow down the type ofvalueto its original typeTin the scope after the assertion. -
assertEquals<T>(actual: T, expected: T, message?: string): asserts actual is T: This function asserts thatactualis strictly equal (===) toexpected. If they are not equal, it throws anErrorwith an optionalmessage. It should also use anassertsclause. -
assertIsString(value: any, message?: string): asserts value is string: This function asserts that the givenvalueis a string. If not, it throws anErrorwith an optionalmessage. -
assertIsNumber(value: any, message?: string): asserts value is number: This function asserts that the givenvalueis a number. If not, it throws anErrorwith an optionalmessage. -
assertIsBoolean(value: any, message?: string): asserts value is boolean: This function asserts that the givenvalueis a boolean. If not, it throws anErrorwith an optionalmessage. -
assertIsObject(value: any, message?: string): asserts value is object: This function asserts that the givenvalueis an object (and notnull). If not, it throws anErrorwith an optionalmessage. -
assertIsArray(value: any, message?: string): asserts value is Array<any>: This function asserts that the givenvalueis an array. If not, it throws anErrorwith an optionalmessage.
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
assertsclause). - If the assertion condition is not met, an
Errorobject should be thrown. - The error message should be informative, ideally including the provided
messageor a default one.
Edge Cases to Consider:
nullandundefinedvalues.- The
assertfunction specifically: what constitutes a "truthy" value? (Standard JavaScript truthiness rules apply). - The
assertIsObjectfunction:typeof nullreturns "object", so you need to handlenullexplicitly.
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
messagestring for custom error reporting. - The
assertstype 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
assertskeyword 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 stringmeans ifassertIsStringcompletes without throwing,valuecan be treated as astringin the subsequent code. - Pay close attention to the
typeofoperator's behavior, especially withnull. - Consider how to construct informative default error messages when no custom message is provided.
- For
assertIsObject, remember thatnullalso hastypeof null === 'object'. You'll need an additional check fornull.