How can one define a function type in typescript that includes varying or extra parameters?

// define callbacks
const checkValue = (key: string, value: unknown) => {
    if (typeof value !== 'number' || Number.isNaN(value)) throw new Error('error ' + key)
    return value
}

const checkRange = (key: string, value: unknown, min: number, max: number = Infinity) => {
    if (typeof value !== 'number' || Number.isNaN(value)) throw new Error('error ' + key)
    if (value < min || value > max) throw new Error('error')
    return value
}

const checkSomething = (key: string, value: unknown, something: number) => {
    if (typeof value !== 'number' || Number.isNaN(value)) throw new Error('error ' + key)
    if (value === something) throw new Error('error')
    return value
}

type Object = { [P: string]: unknown }

const testObject: Record<string, unknown> = { one: 1, two: 'two' }

// main

const evaluate = <
    O extends Object,
    F extends (key: string, val: unknown) => ReturnType<F> 
>(obj: O, prop: keyof O, vfunc: F) => {
    if (typeof prop !== 'string') throw new Error('error')
    return vfunc(prop, obj[prop])
}

const evaluateTwo = <
    O extends Object,
    F extends (key: string, val: unknown, ...args: unknown[]) => ReturnType<F> 
>(obj: O, prop: keyof O, vfunc: F , ...args: unknown[]) => {
    if (typeof prop !== 'string') throw new Error('error')
    return vfunc(prop, obj[prop], ...args)
}

evaluate(testObject, 'one', checkValue) // good
evaluate(testObject, 'one', checkRange) // error as expected

// the following expression statements should not fail

evaluateTwo(testObject, 'one', checkRange) // should say missing `min` arg
evaluateTwo(testObject, 'one', checkRange, 1)  
evaluateTwo(testObject, 'one', checkRange, 1, 10)

evaluateTwo(testObject, 'one', checkSomething, 1) // should say wrong type number instead of string 

In the snippet above, I have defined callback functions checkValue, checkRange, and checkSomething, and showcased the usage of evaluate and evaluateTwo functions for different scenarios. The code also includes error messages and playground links for further reference.

Answer №1

If you're aiming to ensure that args aligns with the remaining parameters for F following key: string, val: unknown, you will need an additional generic type parameter for the type of args:

const evaluate_two = <
    O extends Obj,
    A extends any[],
    F extends (key: string, val: unknown, ...args: A) => ReturnType<F> 
>(obj: O, prop: keyof O, vfunc: F, ...args: A) => {
    if (typeof prop !== 'string') throw new Error('error')
    return vfunc(prop, obj[prop], ...args)
}

Now you will experience the expected behavior:

// const callback2: (key: string, value: unknown, min: number, max?: number) => number
evaluate_two(tesstobj, 'one', callback2) // error, too few arguments
evaluate_two(tesstobj, 'one', callback2, 1)
evaluate_two(tesstobj, 'one', callback2, 1, 10)

// const callback3: (key: string, value: unknown, something: string) => string
evaluate_two(tesstobj, 'one', callback3, 1) // error, number is not string

Link to code on Playground

Answer №2

You have the option to provide a callback, with updated code

// callbacks
const callback1 = (key: string, value: unknown) => {
    if (typeof value !== 'number' || Number.isNaN(value)) throw new Error('error ' + key)
    return value
}

const callback2 = (key: string, value: unknown, min: number, max: number = Infinity) => {
    if (typeof value !== 'number' || Number.isNaN(value)) throw new Error('error ' + key)
    if (value < min || value > max) throw new Error('error')
    return value
}

const callback3 = (key: string, value: unknown, something: number) => {
    if (typeof value !== 'number' || Number.isNaN(value)) throw new Error('error ' + key)
    if (value === something) throw new Error('error')
    return value
}

type Obj = { [P: string]: unknown }

const tesstobj: Record<string, unknown> = { one: 1, two: 'two' }

const evaluate_two = <
    O extends Obj,
    F1 extends () => ReturnType<F1>
>(obj: O, prop: keyof O, vfunc: F1) => {
    if (typeof prop !== 'string') throw new Error('error')
    return vfunc();
}

evaluate_two(tesstobj, 'one', () => {
    callback1('one', tesstobj['one']);
})

evaluate_two(tesstobj, 'one', () => {
    const temp_min = -10;
    const temp_max = 10;
    callback2('one', tesstobj['one'], temp_min, temp_max);
})

evaluate_two(tesstobj, 'one', () => {
    const something = 100;
    callback3('one', tesstobj['one'], something);
})

Update (improved answer based on @bogdanoff's suggestion):

// callbacks
const callback1 = (key: string, value: unknown) => {
    if (typeof value !== 'number' || Number.isNaN(value)) throw new Error('error ' + key)
    return value
}

const callback2 = (key: string, value: unknown, min: number, max: number = Infinity) => {
    if (typeof value !== 'number' || Number.isNaN(value)) throw new Error('error ' + key)
    if (value < min || value > max) throw new Error('error')
    return value
}

const callback3 = (key: string, value: unknown, something: number) => {
    if (typeof value !== 'number' || Number.isNaN(value)) throw new Error('error ' + key)
    if (value === something) throw new Error('error')
    return value
}

type Obj = { [P: string]: unknown }

const tesstobj: Record<string, unknown> = { one: 1, two: 'two' }

const evaluate = <
    O extends Obj,
    F extends (key: string, val: unknown) => ReturnType<F> // <--
>(obj: O, prop: keyof O, vfunc: F) => {
    if (typeof prop !== 'string') throw new Error('error')
    return vfunc(prop, obj[prop])
}

const evaluate_two = <
    O extends Obj,
    F extends (key: string, val: unknown, ...args: any[]) => ReturnType<F> // <--
>(obj: O, prop: keyof O, vfunc: F , ...args: any[]) => {
    if (typeof prop !== 'string') throw new Error('error')
    return vfunc(prop, obj[prop], ...args)
}

evaluate_two(tesstobj, 'one', callback1) 
evaluate_two(tesstobj, 'one', callback2) 
evaluate_two(tesstobj, 'one', callback3) 

simply adjust args types from unknown[] to any[]. Trust this modification helps.

Answer №3

After reviewing @jcalz's solution, I found that while it did resolve the issue, there was a slight drawback. It allowed additional arguments to be passed into the evaluate_two function even though its callback function did not require any, as demonstrated in this playground example.

To rectify this, I created a custom type that emulates TypeScript's Parameters utility type but excludes the first 2 parameters (a1 and a2) of a function.

type ParametersFromIndex2<T extends (a1: any, a2: any, ...args: any) => any> = T extends (a1: any, a2: any, ...args: infer P) => any ? P : never

Full Code Snippet

// Callback functions
const callback1 = (key: string, value: unknown) => {
    if (typeof value !== 'number' || Number.isNaN(value)) throw new Error('error ' + key)
    return value
}

const callback2 = (key: string, value: unknown, min: number, max: number = Infinity) => {
    if (typeof value !== 'number' || Number.isNaN(value)) throw new Error('error ' + key)
    if (value < min || value > max) throw new Error('error')
    return value
}

const callback3 = (key: string, value: unknown, something: string) => {
    if (typeof value !== 'string') throw new Error('not string')
    return value
}

// Custom type to handle callback function arguments
type ParametersFromIndex2<T extends (a1: any, a2: any, ...args: any) => any> = T extends (a1: any, a2: any, ...args: infer P) => any ? P : never

// Function to evaluate callback functions with custom type for arguments
const evaluateNew = <
    O extends R,
    F extends (k: string, v: unknown, ...args: any) => ReturnType<F>,
>(obj: O, prop: keyof O, callback: F, ...args: ParametersFromIndex2<F>) => {
    if (typeof prop !== 'string') throw new Error('error')
    return callback(prop, obj[prop], ...args)
}

// Testing callback functions with various arguments
evaluateNew(tesstobj, "one", callback1)
evaluateNew(tesstobj, "one", callback1)
evaluateNew(tesstobj, "one", callback1, "error")
evaluateNew(tesstobj, "one", callback1, 1)

// Additional testing for callback2 and callback3 functions...

Playground link for testing

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

Maintain form activity post submission using jQuery's addClass and removeClass methods

There is a button on my website that, when clicked, reveals a form. The code I am using for this functionality is: $('#theform').addClass('hidden') $('#theform').removeClass('hidden'); <div id="theform" c ...

Challenges encountered when testing middleware in a TypeScript Node.js Express project

I have been exploring the repository at https://github.com/goldbergyoni/nodebestpractices to enhance my understanding of nodejs best practices. Below is a middleware I developed: import { NextFunction, Request, Response } from "express"; import ...

What is the process for creating a map in which the value type is determined by the key type?

Is it feasible to formulate a Map<K, V> in a manner where the type of the value relies on the type of the key, without explicitly specifying the key's type upon initializing the map? For instance: abstract class BaseA { a() {} } class ConcreteA1 ...

Assigning values to props in a Vue component

I created a Vue component that is supposed to receive the "uploadedFile" prop, but it's not functioning properly. I am only getting the correct information in the $attrs: https://i.sstatic.net/crXmH.png Here is my component: Vue.component('t ...

How come my web page is not displaying the guages from guage.js?

I am utilizing guage.js to create a graph based on this Fiddle. However, upon adding the same code to my website, the rendering does not appear as expected: https://i.sstatic.net/fIkQr.png Upon inspecting in Chrome developer tools, I noticed that the ele ...

Having trouble rendering Next.JS dynamic pages using router.push()? Find out how to fix routing issues in your

The issue I am facing revolves around the visibility of the navbar component when using route.push(). It appears that the navbar is not showing up on the page, while the rest of the content including the footer is displayed. My goal is to navigate to a dy ...

What is the process for setting up a resthook trigger in Zapier?

I'm currently integrating Zapier into my Angular application, but I'm struggling with setting up a REST hook trigger in Zapier and using that URL within my app. I need to be able to call the REST hook URL every time a new customer is created and ...

Is It Possible to Use Separate Stylesheets for Different Web Browsers?

I have been trying to implement a JavaScript code that loads a specific stylesheet based on the user's browser. However, it seems like the code is not functioning correctly as none of the stylesheets are being displayed. I have meticulously reviewed t ...

Patience is key when waiting for Ajax response data in Vue.js

Within my Vue component, I am attempting to retrieve data from an API using axios. <template> <div> This is Default child component {{tools[0].name}} </div> </template> <script> import { CustomJS } fr ...

Ways to reveal a concealed element by simply clicking on a different element

Check out this example on JSFiddle: http://fiddle.jshell.net/ggw6hqsj/1/ Currently, I am facing an issue where: Clicking the first button hides the second button. However, I am struggling with making the hidden button reappear when the first but ...

Exploring Next.js Font Styling and Utilizing CSS Variables

I'm attempting to implement the "next" method for adding fonts, but I find the process described quite complex just to preload a font. I experimented with exporting a function to create the font and then using a variable tag to generate a CSS variabl ...

When executing code in React JS, I encountered no errors, but the output did not match my expectations

I am facing a challenge with running the Hello World program in React JS using webpack. Attached below is the project structure for reference: https://i.stack.imgur.com/tQXeK.png Upon executing the npm run dev command in the CLI, the browser launches bu ...

Having trouble getting card animations to slide down using React Spring

I am currently learning React and attempting to create a slide-down animation for my div element using react-spring. However, I am facing an issue where the slide-down effect is not functioning as expected even though I followed a tutorial for implementati ...

Is it achievable to animate dynamic height using CSS? Open to JS alternatives as well

I am currently utilizing AngularJS, which enables me to utilize ng-show and ng-hide in order to display or hide elements based on a logical condition. My goal is to animate the size changes of the container when the child objects are shown or hidden, creat ...

Displaying two distinct tables utilizing ng-repeat by employing customized directives

I'm facing an issue with a custom directive that generates tables and is appearing twice on my index page. The data for these tables comes from the $scope.globalrows variable. Even though globalrows contains 2 arrays, it always displays the second arr ...

Tally each div individually and display the count within each div, instead of showing the total count across

I have come across various solutions that show the total number of certain special divs, such as: $('.someclass').length However, I am facing a different challenge. I want to sequentially count each div with a numerical sequence. For instance, ...

Can Angular JS apply the uppercase filter to a boolean value?

My Angular 1.4.12 binding looks like this: {{ mob.mobDataSettings[7].value | uppercase }} The first part is a boolean value from a JSON file, which can be either true or false. But when rendered in HTML, it is not showing up as uppercase (e.g. TRUE), in ...

Tips for crafting paragraphs that double as sieves

I'm trying to simplify and shorten this code. I have three HTML paragraphs acting as filters: "all", "positive," and "negative", referring to reviews. Each has a corresponding div for reviews: "allcont", "poscont", and "negcont". Clicking on any of th ...

React - Login page briefly appears while loading

Hello, I have built a React application that consists of a Login page, Loading page, and the main app itself. However, there is a small issue where after a user logs in, instead of smoothly transitioning to the Loading page until the data is loaded, the L ...

What is the module system that fabric composer utilizes for its logic JavaScript files?

I am currently in the process of creating a business-network-definition for Hyperledger using Fabric (based on generator-hyperledger-fabric). Everything has been running smoothly so far, but as we move onto our Proof of Concept (PoC), a few questions have ...