Utilizing generic optional parameters in Typescript function overloads

I am currently attempting to create a higher-order function that wraps the input function and caches the result of the most recent call as a side effect. The basic function (withCache) is structured as follows:

function cache(key: string, value: any) {
    //Caching logic implemented here
}

function withCache<R>(key: string, fn: (...args: any[]) => R): (...args: any[]) => R {
    return (...args) => {
        const res = fn(...args);
        cache(key, res);
        return res;
    }
}

const foo = (x: number, y: number) => x + y;
const fooWithCache = withCache("foo", foo);
let fooResult1 = fooWithCache(1, 2); // allowed :)
let fooResult2 = fooWithCache(1, 2, 3, 4, 5, 6) // also allowed :(

To ensure type safety, I can use function overloads like this:

function withCache<R>(key: string, fn: () => R): () => R
function withCache<R, T1>(key: string, fn: (a: T1) => R): (a: T1) => R
function withCache<R, T1, T2>(key: string, fn: (a: T1, b: T2) => R): (a: T1, b: T2) => R
function withCache<R>(key: string, fn: (...args: any[]) => R): (...args: any[]) => R {
    // implementation ...
}

const foo = (x: number, y: number) => x + y;
const fooWithCache = withCache("foo", foo);
let fooResult1 = fooWithCache(1, 2); // allowed :)
let fooResult2 = fooWithCache(1, 2, 3, 4, 5, 6) // not allowed :)

The challenge arises when trying to allow functions with optional arguments where Typescript isn't selecting the correct overload for withCache, resulting in an unexpected signature for fooWithCache. Is there a way to resolve this issue?

(As a side note, is there any way to declare the overloads so I don't have to repeat each overload's function type (...) => R?)

Edit:

Resolved my secondary question about repetitive function type declarations by defining it separately:

type Function1<T1, R> = (a: T1) => R;
// ...
function withCache<T1, R>(fn: Function1<T1, R>): Function1<T1, R>;

Edit:

How would this work for an asynchronous function (assuming you wanted to cache the result and not the Promise itself)? You could certainly do this:

function withCache<F extends Function>(fn: F) {
  return (key: string) =>
      ((...args) => 
        //Wrap in a Promise so we can handle sync or async
        Promise.resolve(fn(...args)).then(res => { cache(key, res); return res; })
    ) as any as F; //Really want F or (...args) => Promise<returntypeof F>
}

However, using this approach would be unsafe with synchronous functions:

//Async function
const bar = (x: number) => Promise.resolve({ x });
let barRes = withCache(bar)("bar")(1).x; //Not allowed :)

//Sync function
const foo = (x: number) => ({ x });
let fooRes = withCache(foo)("bar")(1).x; //Allowed, but TS assumes fooRes is an object :(

Is there a safeguard against this? Or a way to create a function that works safely for both scenarios?

Summary: @jcalz's answer is correct. In cases where synchronous functions can be assumed, or where working directly with Promises instead of their resolved values is acceptable, asserting the function type might be safe. However, handling the sync-or-async situation described above necessitates language improvements that are still pending development.

Answer №1

When it comes to choosing overloads, the process involves going down the list and selecting the first one that matches the criteria.

Take a look at the code snippet below, which compiles successfully:

declare let f: (a: any, b?: any) => void;
declare let g: (a: any) => void;
g = f; // works fine

The function f can take either one or two parameters, while g is defined as expecting just one parameter. This assignment of f to g is possible because anywhere a function with one parameter is needed, a function with one or two parameters will also suffice due to the optional nature of the second parameter.

You can also perform the reverse assignment:

f = g; // works too

This interchangeability shows that functions of both types are mutually assignable, even though they are not exactly the same, creating a slight inconsistency.


Considering only these two overload options:

function withCache<R, T1>(key: string, fn: (a: T1) => R): (a: T1) => R
function withCache<R, T1, T2>(key: string, fn: (a: T1, b?: T2) => R): (a: T1, b?: T2) => R

The previous discussion regarding f and g suggests that anything matching one of these overloads will inevitably match the other. Therefore, the ordering in which they appear determines the selection. Unfortunately, you cannot utilize both simultaneously.

Before attempting to establish a compromise set of overloads for desired functionality, perhaps we should reconsider:


Could a type-safe version of withCache() be sufficient? Consider this approach:

function withCache<F extends Function>(key: string, fn: F): F {     
    // implementation ...
}

This solution eliminates the need for overloads, ensuring that the return value aligns with the type specified for the fn parameter:

const foo = (x: number, y?: number) => x;
const fooWithCache = withCache("foo", foo); // (x: number, y?: number) => number
let fooResult1 = fooWithCache(1); // permitted :)
let fooResult2 = fooWithCache(1, 2) // permitted :)

Does this approach meet your requirements? Best of luck!

Similar questions

If you have not found the answer to your question or you are interested in this topic, then look at other similar questions below or use the search

Intelligent prediction of data type

Is there a method in TypeScript to infer the type of a variable after a function check? const variable: string | undefined = someFunction() variable && setAppTitle(variable) // ensuring that the variable is a defined string However, in my application, thi ...

Typescript loading icon directive

Seeking to create an AngularJS directive in TypeScript that wraps each $http get request with a boolean parameter "isShow" to monitor the request status and dynamically show/hide the HTML element depending on it (without utilizing $scope or $watch). Any ...

"ENUM value is stored in the event target value's handle property

My issue lies with the event.target.value that returns a 'String' value, and I want it to be recognized as an ENUM type. Here is my current code: export enum Permissions { OnlyMe, Everyone, SelectedPerson } ... <FormControl> & ...

The reset() function in Angular does not set form controls to empty values

When following the Hero guide, I encountered an issue while trying to reset all fields in the model using the provided code snippet. this.form.reset({ "firstName": "", "lastName": "bzz", "reporter": "" }); The problem arises when only non-empty fi ...

FlatList: Can you list out the attributes of ListRenderItemInfo<>?

I am attempting to utilize a FlatList in the following manner: <FlatList data={vehicles} horizontal={false} scrollEnabled renderItem={({ vehicle}) => <VehicleContainer vehicle={vehicle} />} ...

Encountering Typescript errors while compiling an Angular module with AOT enabled

I am currently in the process of manually constructing an Angular module with Webpack, opting not to use the CLI. While a normal build is functioning without any issues, encountering errors during an AOT build! Here's how my tsconfig.aot.json file ...

Setting checkbox values using patchValue in Angular programming

I'm facing an issue with reusing my create-form to edit the form values. The checkbox works fine when creating a form, but when I try to edit the form, the values don't get updated on the checkbox. Below is the code snippet that I have been worki ...

What is the proper way to input a Response object retrieved from a fetch request?

I am currently handling parallel requests for multiple fetches and I would like to define results as an array of response objects instead of just a general array of type any. However, I am uncertain about how to accomplish this. I attempted to research "ho ...

numerous slices of toasted bread for the latest version of Ionic

I'm looking to implement multiple toasts in Ionic framework v4, but I'm not sure how to go about coding it. I attempted to implement multiple toasts in Ionic v3, but it didn't meet my requirements. import { Component, OnInit } from '@ ...

The componentWillReceiveProps method is not triggered when a property is removed from an object prop

Just starting out with React, so I could really use some assistance from the community! I am working with a prop called 'sampleProp' which is defined as follows: sampleProp = {key:0, value:[]} When I click a button, I am trying to remo ...

How can I make TypeScript mimic the ability of JavaScript object wrappers to determine whether a primitive value has a particular "property"?

When using XMLValidator, the return value of .validate function can be either true or ValidationError, but this may not be entirely accurate (please refer to my update). The ValidationError object includes an err property. validate( xmlData: string, opti ...

Anonymous function bundle where the imported namespace is undefined

Here is the code snippet I am working with: import * as Phaser from 'phaser'; new Phaser.Game({ width:300, height:300, scale: { mode: Phaser.Scale.FIT, }, type: Phaser.AUTO, scene: { create() {} }, }); Upon compi ...

What is the process for integrating a new token into an established programming language, such as TypeScript?

Utilizing setMonarchTokensProvider allows me to define tokens, but it has limitations. I am able to create a new language or override existing typescript, however, this does not provide me with access to the rest of the typescript tokens that I still requi ...

io-ts: Defining mandatory and optional keys within an object using a literal union

I am currently in the process of defining a new codec using io-ts. Once completed, I want the structure to resemble the following: type General = unknown; type SupportedEnv = 'required' | 'optional' type Supported = { required: Gene ...

Steps to specify a prefix for declaring a string data type:

Can we define a string type that must start with a specific prefix? For instance, like this: type Path = 'site/' + string; let path1: Path = 'site/index'; // Valid let path2: Path = 'app/index'; // Invalid ...

What is the most effective way to ensure that generic type constraints are met?

How can I ensure that a generic parameter used in a class can only be one of my custom types, such as ClassA, ClassB, or ClassC, and not meaningless types like int or long? One idea I had was to make these custom types implement the same empty interface, ...

What is causing certain code to be unable to iterate over values in a map in TypeScript?

Exploring various TypeScript idioms showcased in the responses to this Stack Overflow post (Iterating over Typescript Map) on Codepen. Below is my code snippet. class KeyType { style: number; constructor(style) { this.style = style; }; } fu ...

Using jQuery to bind data to Angular's *ngFor directive

I am currently in the process of customizing a horizontal timeline resembling https://codepen.io/ritz078/pen/LGRWjE/. The demo includes hardcoded dates which I want to replace with an array of Dates (timelineParsedDates) <ol> <li><a href ...

The functionality of the Vert.x event bus client is limited in Angular 7 when not used within a constructor

I have been attempting to integrate the vertx-eventbus-client.js 3.8.3 into my Angular web project with some success. Initially, the following code worked perfectly: declare const EventBus: any; @Injectable({ providedIn: 'root' }) export cl ...

proceed to another page after choosing specific dates for submission

I have created the following code to build a booking calendar. I would like it so that when I click on submit, it will redirect to a different page by navigating. The function should check if the start date is greater than the current date and the end date ...