Implementing Liquid Types in TypeScript
Liquid types, inspired by functional programming concepts, allow for dynamic behavior within statically typed systems. This challenge asks you to implement a simplified version of liquid types in TypeScript, enabling functions to return values with conditionally determined types. This is useful for creating flexible APIs and managing complex data structures where the output type depends on runtime conditions.
Problem Description
Your task is to create a TypeScript function that simulates the behavior of liquid types. This function will take a value and a "predicate" function as input. The predicate function will determine, based on the value, which of two predefined return types the main function should produce.
Specifically, you need to:
- Define two distinct return types: Let's call them
TypeAandTypeB. These can be simple interfaces or union types. - Create a main function: This function will accept:
- A
valueof a generic typeT. - A
predicatefunction that takesvalue(typeT) and returns aboolean.
- A
- Implement conditional return:
- If the
predicatereturnstruefor the givenvalue, the main function should return a value conforming toTypeA. - If the
predicatereturnsfalse, the main function should return a value conforming toTypeB.
- If the
- Ensure type safety: The return type of the main function should be a union of
TypeAandTypeB(TypeA | TypeB). TypeScript should be able to infer this union type correctly.
Key Requirements:
- The solution must be implemented entirely in TypeScript.
- The main function should be generic enough to accept any input
valuetype. - The predicate function's return value should directly control the output type.
Expected Behavior:
When the main function is called, its return type should be TypeA | TypeB. However, the specific type of the returned value at runtime will be either TypeA or TypeB, determined by the predicate. TypeScript should help you narrow down the type based on checks performed after calling the main function.
Examples
Example 1:
interface SuccessResponse {
status: 'success';
data: string;
}
interface ErrorResponse {
status: 'error';
message: string;
}
// Define the main function (you need to implement this)
// function processData<T>(value: T, predicate: (val: T) => boolean): SuccessResponse | ErrorResponse;
const input = "valid_data";
const predicate = (val: string) => val.startsWith("valid");
const result = processData(input, predicate);
// At this point, TypeScript should know `result` is `SuccessResponse | ErrorResponse`.
// You can then use type guards to determine the actual type.
if (result.status === 'success') {
// Inside this block, TypeScript should infer `result` as `SuccessResponse`.
console.log(`Success: ${result.data.toUpperCase()}`);
} else {
// Inside this block, TypeScript should infer `result` as `ErrorResponse`.
console.log(`Error: ${result.message.toLowerCase()}`);
}
Expected Output (for Example 1):
Success: VALID_DATA
Explanation:
The input "valid_data" passes the predicate (since it starts with "valid"), so processData returns a SuccessResponse. The type guard result.status === 'success' correctly narrows down the type of result to SuccessResponse within the if block, allowing access to result.data.
Example 2:
interface UserInfo {
id: number;
username: string;
}
interface GuestInfo {
role: 'guest';
permissions: string[];
}
// Define the main function (you need to implement this)
// function getUserOrGuest<T>(value: T, isUser: (val: T) => boolean): UserInfo | GuestInfo;
const userData = { id: 123, username: "alice" };
const isUserPredicate = (val: typeof userData) => typeof val.id === 'number';
const userInfo = getUserOrGuest(userData, isUserPredicate);
if ('username' in userInfo) {
// TypeScript should infer `userInfo` as `UserInfo`.
console.log(`Welcome, ${userInfo.username}!`);
} else {
// TypeScript should infer `userInfo` as `GuestInfo`.
console.log(`Guest with permissions: ${userInfo.permissions.join(', ')}`);
}
Expected Output (for Example 2):
Welcome, alice!
Explanation:
The userData object contains an id of type number, so the isUserPredicate returns true. getUserOrGuest returns a UserInfo object. The in operator acts as a type guard, correctly inferring userInfo as UserInfo in the if block.
Constraints
- The implementation of the main function should be a single function.
- The
TypeAandTypeBshould be distinct types (e.g., not simple aliases of each other). - The solution should rely on TypeScript's type system and not on runtime checks that bypass type safety.
- The solution should be demonstrably type-safe. A type error should occur if you try to access a property that doesn't exist on the narrowed type without a proper type guard.
Notes
Consider how you can leverage TypeScript's conditional types or union types to achieve the dynamic return type. Think about how the generic type T and the predicate's signature should be defined to ensure maximum type safety. The goal is to write a function that is both flexible and predictable in its type behavior.