Deriving a universal parameter from a function provided as an argument

My function can take in different adapters along with their optional options.

// Query adapter type 1
type O1 = { opt: 1 }
const adapter1 = (key: string, options?: O1) => 1
// Query adapter type 2
type O2 = { opt: 2 }
const adapter2 = (key: string, options?: O2) => 2
// There can be numerous query adapters, but they all follow a similar format

As mentioned in the comments, there could be an infinite number of adapters (hence the need for a generic argument in the adapter consuming function). However, all adapters have their Parameters in the common format of

[string, options?: unknown (generic)]

Below is the definition of the adapter consuming function:

// Type definition helper for the adapter
type Fn = <O, R>(key: string, options?: O) => R

// 'consumer' function which accepts these adapters and their options as an optional argument
const query = <F extends Fn, O extends Parameters<F>[1]>(
  key: string,
  adapter: F,
  options?: O
): ReturnType<F> => adapter(key, options)

I expected TypeScript to correctly infer the options based on the provided arguments, however, that is not the case:

// These should be valid (omitting optional configuration)
query('1', adapter1)
query('2', adapter2)

// These should also be valid (with configuration applied)
query('1config', adapter1, { opt: 1 })
query('2config', adapter2, { opt: 2 })

// These should throw an error due to configuration type mismatch
query('1error', adapter1, { foo: 'bar' })
query('2error', adapter2, { foo: 'bar' })

However, all of these examples are triggering a TypeScript error due to a mismatch in the adapter arguments provided. The error message reads as follows:

Argument of type '(key: string, options?: O1) => number' is not assignable to parameter of type 'Fn'.
  Types of parameters 'options' and 'options' are incompatible.
    Type 'O | undefined' is not assignable to type 'O1 | undefined'.
      Type 'O' is not assignable to type 'O1 | undefined'.

In this scenario, one could resolve the issue by changing O to O extends O1 | O2 | undefined. However, since there can be theoretically unlimited variations in adapter options, I still need my consumer function to accurately infer the option object for enhanced type safety when users interact with the query function and specify the options object.

The actual problem is a bit more complex with callbacks, but this example nicely illustrates the issue. Please refrain from commenting on the use of

(key, options) => adapter(key, options)
for direct invocation, as it is beyond the scope of the question.

Here is the TS Playground for you to experiment with

Answer №1

The issue at hand revolves around the incorrect scope of generic type parameters in your Fn type. The current definition of Fn allows the caller to choose the types for O and R, but the implementation must be able to handle any choice made by the caller. This contradicts the behavior of both adapter1 and adapter2. To rectify this, consider the following correct definition:

type Fn<O, R> = (key: string, options?: O) => R

With this definition, the implementer specifies the types for O and R, and the Fn<O, R> can only be used with those particular choices. For more insights on generic type parameter scoping, refer to typescript difference between placement of generics arguments.

To resolve the issue, you should adjust Fn to Fn<any, any> in your query() and eliminate the unnecessary O type parameter:

const query = <F extends Fn<any, any>>(
    key: string, adapter: F, options?: Parameters<F>[1]
): ReturnType<F> => adapter(key, options)

query('1', adapter1) // working
query('2', adapter2) // working
query('1config', adapter1, { opt: 1 }) // working
query('2config', adapter2, { opt: 2 }) // working
query('1error', adapter1, {}) // error, missing opt
query('2error', adapter2, { foo: 'bar' }) // error, unexpected foo

While this solves the original question, there's another issue worth mentioning:


When using the Parameters and ReturnType utility types with a generic parameter, the compiler struggles with type checking generic conditional types. This means that some errors may go unnoticed, like in the following case:

const badQuery = <F extends Fn<any, any>>(
    key: string, adapter: F, options?: Parameters<F>[1]
): ReturnType<F> => adapter(key, 389398) // no error!!

For more reliable results, consider making query() directly generic in O and

R</code, instead of relying on <code>F
to infer the types:

const query = <O, R>(
    key: string, adapter: Fn<O, R>, options?: O
): R => adapter(key, options); 

While this maintains the same behavior from the caller's perspective, it also helps in catching implementation errors:

const badQuery = <O, R>(
    key: string, adapter: Fn<O, R>, options?: O
): R => adapter(key, 389398); // expected error now caught

Explore the Playground link to code

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

Scrolling to the bottom of an ion-content in Ionic 4

I am currently developing a chat page with Ionic 4 and I'm attempting to implement an automatic scroll feature to the bottom of the page. However, the method I tried using doesn't seem to be working as expected: import { IonContent } from "@ioni ...

Ways to trigger the keyup function on a textbox for each dynamically generated form in Angular8

When dynamically generating a form, I bind the calculateBCT function to a textbox like this: <input matInput type="text" (keyup)="calculateBCT($event)" formControlName="avgBCT">, and display the result in another textbox ...

Efficiently convert Map keys into a Set in Javascript without the need to copy and rebuild the set

Even though I am capable of const set = new Set(map.keys()) I don't want to have to rebuild the set. Additionally, I prefer not to create a duplicate set for the return value. The function responsible for returning this set should also have the abili ...

Working with Typescript and JSX in React for event handling

I'm currently facing an issue with updating the state in a React component I'm developing using TypeScript (React with Addons 0.13.3, Typescript 1.6.0-dev.20150804, definition file from ). /// <reference path="react/react-addons.d.ts" /> i ...

What is the process of assigning a value type to a generic key type of an object in typescript?

I am currently working on developing a function that can merge and sort pre-sorted arrays into one single sorted array. This function takes an array of arrays containing objects, along with a specified key for comparison purposes. It is important to ensure ...

Merge generic nested objects A and B deeply, ensuring that in case of duplicate properties, A's will take precedence over B's

Two mysterious (generic) nested objects with a similar structure are in play: const A = { one: { two: { three: { func1: () => null, }, }, }, } const B = { one: { two: { three: { func2: () => null, ...

Creating a generic anonymous class in TypeScript can be achieved by specifying the class body

The title might be a bit misleading, but I'm struggling to articulate my issue. I aim to pass a generic class (or just the class body) as an argument to a function. The provided class should then infer its generics automatically. class Builder<EX ...

Discover the highest value within an array of objects, along with any numerical object attributes that have a value greater than zero

Considering an array of objects structured as follows: [{ "202201": { "WO": 900, "WS": 0, "SY": 0.915, "LY": 0.98, "CT": 75 }, "202202" ...

Personalized data type for the Request interface in express.js

I'm attempting to modify the type of query in Express.js Request namespace. I have already tried using a custom attribute, but it seems that this approach does not work if the attribute is already declared in @types (it only works for new attributes a ...

Incorporating a Component with lazy-loading capabilities into the HTML of another Component in Angular 2+

Striving to incorporate lazy loading in Angular 2, I have successfully implemented lazy loading by following this helpful guide. Within my application, I have two components - home1 and home2. Home1 showcases the top news section, while home2 is dedicated ...

Using GraphQL to set default values in data within a useEffect hook can lead to never

Here's the code snippet that I'm working with: const [localState, setLocalState] = useState<StateType[]>([]); const { data = { attribute: [] }, loading } = useQuery<DataType>(QUERY, { variables: { id: client && client.id ...

Enabling Javascript compilation while overlooking typescript errors

Currently, I am working in VS Code with create-react-app using TypeScript. Whenever there are type errors, they show up in the browser and prevent compilation by TypeScript. I am looking for a way to only see these errors in the Google console and termin ...

Utilizing classes as types in TypeScript

Why is it possible to use a class as a type in TypeScript, like word: Word in the provided code snippet? class Dict { private words: Words = {}; // I am curious about this specific line add(word: Word) { if (!this.words[word.term]) { this.wor ...

Creating a method that can adopt the return type of the nested function?

I have a function that takes in a callback and returns the value obtained using the useSelector hook from the react-redux library. Is there a way to utilize the return type of useSelector within my wrapper function? import { shallowEqual, useSelector } f ...

What is the best way to incorporate auto-completion into a browser-based editor using Monaco?

Recently, I embarked on a project to develop a browser-based editor using monaco and antlr for a unique programming language. Following an excellent guide, I found at , gave me a great start. While Monaco provides basic suggestions with ctrl + space, I am ...

Creating Dynamic Forms in React with Typescript: A Step-by-Step Guide to Adding Form Elements with an onClick Event Handler

I am looking to create a dynamic generation of TextFields and then store their values in an array within the state. Here are my imports: import TextField from '@material-ui/core/TextField'; import Button from '@material-ui/core/Button&apos ...

Guide to mocking the 'git-simple' branchLocal function using jest.mock

Utilizing the simple-git package, I have implemented the following function: import simpleGit from 'simple-git'; /** * The function returns the ticket Id if present in the branch name * @returns ticket Id */ export const getTicketIdFromBranch ...

What are the recommended techniques for utilizing prototypal and prototype-based inheritance in modern JavaScript (ES6) and TypeScript?

Some older discussions on Javascript prototypal inheritance & delegation can be found below: Benefits of prototypal inheritance over classical? classical inheritance vs prototypal inheritance in javascript I am curious about the current (2018) recom ...

What is the best method for eliminating the .vue extension in TypeScript imports within Vue.JS?

I recently created a project using vue-cli3 and decided to incorporate TypeScript for added type safety. Here is a snippet from my src/app.vue file: <template> <div id="app"> <hello-world msg="test"/> </div> </template& ...

The offline functionality of the Angular Progressive Web App(PWA) is experiencing difficulties

As per the official guidelines, I attempted to create a PWA that functions in offline mode using pure PWA without angular-cli. However, despite following the official instructions, I was unable to make it work offline. The document in question can be foun ...