Hone logo
Hone
Problems

TypeScript Function Overload Resolution Challenge

TypeScript's powerful type system allows for function overloads, enabling a single function to have different signatures based on its arguments. This challenge tests your understanding of how TypeScript resolves which overload to use when a function is called, and how to implement a custom overload resolution strategy. This skill is crucial for building robust and user-friendly libraries and APIs in TypeScript.

Problem Description

You are tasked with creating a utility that simulates TypeScript's function overload resolution. Given a function signature with multiple overloads and a specific call signature, your task is to determine which of the provided overloads (if any) best matches the given call signature.

Requirements:

  1. resolveOverload Function: Implement a function named resolveOverload that accepts two arguments:

    • overloads: An array of function signatures (represented as strings or a more structured type you define). Each signature should define the return type and the types of its parameters.
    • callSignature: A representation of the arguments being passed to the function.
  2. Matching Logic: The resolveOverload function should identify the best matching overload for the callSignature. The matching logic should follow these principles (similar to TypeScript's):

    • Exact Match: An overload is an exact match if its parameter types precisely match the types of the arguments in callSignature.
    • Widening/Narrowing:
      • If an overload has a parameter type that is a supertype of the corresponding argument type (e.g., string can match number if number is assigned to a string variable implicitly, though this is simplified for this exercise), it's a potential match.
      • If an overload has a parameter type that is a subtype of the corresponding argument type (e.g., a specific literal type 5 trying to match a parameter of type number), it's a potential match.
    • Optional Parameters and Rest Parameters: Handle optional parameters (e.g., arg?: string) and rest parameters (e.g., ...args: number[]). A call with fewer arguments than required by an overload might still match if the remaining parameters are optional. A call with more arguments might match if the last parameter is a rest parameter.
    • Ambiguity: If multiple overloads are equally good matches, the first one listed in the overloads array should be chosen.
    • No Match: If no overload can be found to match the callSignature, return a specific indicator (e.g., null or undefined).
  3. Representation: You will need to define a way to represent function signatures and call signatures. For this challenge, you can use simplified string representations or create interfaces/types to represent them. A common approach is to represent a signature as an object with parameters (an array of type strings) and returnType (a string).

Expected Behavior:

The resolveOverload function should return a representation of the best matching overload, or a null/undefined value if no match is found.

Edge Cases to Consider:

  • Functions with no parameters.
  • Functions with only optional parameters.
  • Functions with rest parameters.
  • Calls with zero arguments when overloads expect arguments.
  • Calls with arguments where no overload matches.
  • Empty overloads array.

Examples

Example 1:

// Simplified representation of signatures
interface FunctionSignature {
  parameters: string[];
  returnType: string;
}

const overloads: FunctionSignature[] = [
  { parameters: ["number"], returnType: "string" },
  { parameters: ["string"], returnType: "number" },
  { parameters: ["string", "number"], returnType: "boolean" }
];

const callSignature1 = ["string"]; // Represents a call like someFunc("hello")

// Expected Output:
// { parameters: ["string"], returnType: "number" }
// Explanation: The call signature "string" directly matches the second overload's parameter list.

Example 2:

const overloads: FunctionSignature[] = [
  { parameters: ["number", "number"], returnType: "number" },
  { parameters: ["string"], returnType: "string" },
  { parameters: ["number"], returnType: "string" }
];

const callSignature2 = [10, 20]; // Represents a call like someFunc(10, 20)

// Expected Output:
// { parameters: ["number", "number"], returnType: "number" }
// Explanation: The call signature [10, 20] (two numbers) matches the first overload.

Example 3:

const overloads: FunctionSignature[] = [
  { parameters: ["string"], returnType: "string" },
  { parameters: ["number", "number"], returnType: "number" },
  { parameters: ["string", "string"], returnType: "boolean" }
];

const callSignature3 = ["hello"]; // Represents a call like someFunc("hello")

// Expected Output:
// { parameters: ["string"], returnType: "string" }
// Explanation: The call signature ["hello"] (one string) matches the first overload.

Example 4 (Optional Parameter):

interface FunctionSignature {
  parameters: string[];
  returnType: string;
}

const overloads: FunctionSignature[] = [
  { parameters: ["number"], returnType: "string" },
  { parameters: ["number", "string?"], returnType: "boolean" } // Optional second parameter
];

const callSignature4 = [123]; // Represents a call like someFunc(123)

// Expected Output:
// { parameters: ["number"], returnType: "string" }
// Explanation: The call signature [123] matches the first overload. If it were more complex,
// the optional parameter handling would come into play. Let's refine this.

// Revised Example 4 to show optional parameter matching
const overloadsOptional: FunctionSignature[] = [
  { parameters: ["number"], returnType: "string" },
  { parameters: ["number", "string?"], returnType: "boolean" }
];

const callSignature4_a = [123]; // Call with one arg
const callSignature4_b = [123, "world"]; // Call with two args

// Expected Output for callSignature4_a:
// { parameters: ["number"], returnType: "string" }
// Explanation: The first overload is a direct match for one argument.

// Expected Output for callSignature4_b:
// { parameters: ["number", "string?"], returnType: "boolean" }
// Explanation: The second overload matches as its second parameter is optional.

Example 5 (Rest Parameter):

interface FunctionSignature {
  parameters: string[];
  returnType: string;
}

const overloadsRest: FunctionSignature[] = [
  { parameters: ["...numbers: number[]"], returnType: "number" },
  { parameters: ["string"], returnType: "string" }
];

const callSignature5_a = [1, 2, 3]; // Represents a call like someFunc(1, 2, 3)
const callSignature5_b = ["hello"]; // Represents a call like someFunc("hello")

// Expected Output for callSignature5_a:
// { parameters: ["...numbers: number[]"], returnType: "number" }
// Explanation: The rest parameter overload matches multiple number arguments.

// Expected Output for callSignature5_b:
// { parameters: ["string"], returnType: "string" }
// Explanation: The string overload matches a single string argument.

Constraints

  • The overloads array will contain at least one FunctionSignature.
  • The callSignature will be an array of strings representing argument types (e.g., "number", "string", "string?", "...rest: T[]"). You'll need to parse these.
  • Parameter types will be basic TypeScript types like number, string, boolean, any, unknown, void, and optionally T | U for union types.
  • You can assume simple type compatibility (e.g., number is compatible with any, string is compatible with any).
  • Focus on the structural matching of parameter types and counts, not complex type inference or conditional types.
  • Performance is not a primary concern for this challenge. Readability and correctness of the matching logic are key.

Notes

  • Consider how you will represent the parameter types in your FunctionSignature and callSignature. You might need to parse strings like "string?" or "...rest: number[]".
  • TypeScript's actual overload resolution is sophisticated. For this challenge, focus on a reasonable simulation that captures the core concepts of matching parameter counts and types, including optional and rest parameters.
  • Think about a scoring mechanism or a prioritized approach to determine the "best" match when multiple overloads are plausible.
  • You might want to define a helper function to check if a single argument type is compatible with a single parameter type.
Loading editor...
typescript