Currying disrupts the inference of argument types as the argument list is divided in half, leading to confusion

One of my favorite functions transforms an object into a select option with ease. It's written like this:

type OptionValue = string;
type OptionLabel = string;

export type Option<V extends OptionValue = OptionValue, L extends OptionLabel = OptionLabel> = {
  value: V;
  label: L;
};

type ExtractStrings<T> = Extract<T, string>;

export const toOption =
  <
    ValueKey extends string,
    LabelKey extends string | ((item: ObjectIn) => OptionLabel),
    ObjectIn extends Record<ExtractStrings<ValueKey | LabelKey>, OptionValue>
  >(
    valueKey: ValueKey,
    labelKey: LabelKey,
    objectIn: ObjectIn | null | undefined
  ): Option<string, string> | null  => {
    return null // Implementation doesn't matter here
  };

const myObj = { x: "foo", y: "bar" } as const 

const result = toOption("x", (params) => `${params.x} (${params.y})`, myObj)
const result2 = toOption("x", "y", myObj)

Check it out on TypeScript Playground

The typing system works well and enforces three key constraints:

  1. ValueKey must be present in ObjectIn
  2. If LabelKey is a string, it must exist in ObjectIn
  3. If LabelKey is a function, its parameter should match ObjectIn

I'm now exploring currying the last parameter of the function but encountering issues with type inference. Here's an example:

export const toOption =
  <
    ValueKey extends string,
    LabelKey extends string | ((item: ObjectIn) => OptionLabel),
    ObjectIn extends Record<ExtractStrings <ValueKey | LabelKey>, OptionValue>
  >(
    valueKey: ValueKey,
    labelKey: LabelKey,
  ) => (objectIn: ObjectIn | null | undefined): Option<string, string> | null  => {
    return null // Implementation isn't crucial here
  };

const myObj = { x: "foo", y: "bar" } as const

const result = toOption("x", (params) => `${params.x} (${params.y})`)(myObj)
//   Property 'y' does not exist on type 'Record<"x", string>' ^^^

Try it out on TypeScript Playground

My challenge lies in preserving the assertions I made previously while currying the function. Any suggestions or insights would be greatly appreciated.

Thank you for your valuable input 🙂

Answer â„–1

It seems that your current approach might be attempting to address multiple issues: currying (or technically partial application) and the actual implementation itself.

Instead of embedding the curry logic directly within your method, consider defining separate curry methods to handle it for you. Here is the playground.

Firstly, define some curry types and methods.

type FnCurry2<A,B,C> = (s1: A) => (s2: B) => C
type FnCurry3<A,B,C,D> = (s1: A) => (s2: B) => (s3: C) => D

export const curry2 = <A,B,C>(fn: (a: A, b: B) => C): FnCurry2<A,B,C> =>{
  return (a: A) => (b: B) => fn(a, b);
}

export const curry2r = <A,B,C>(fn: (a: A, b: B) => C): FnCurry2<B,A,C> => {
  return (b: B) => (a: A)  => fn(a, b);
}

export const curry3 = <A,B,C,D>(fn: (a: A, b: B, c: C) => D): FnCurry3<A,B,C,D> =>{
  return (a: A) => (b: B) => (c: C) => fn(a, b, c);
}

export const curry3r = <A,B,C,D>(fn: (a: A, b: B, c: C) => D): FnCurry3<C,A,B,D> => {
  return (c: C) => (a: A) => (b: B) => fn(a, b, c);
}

Next, standardize your method for normal usage.

export const toOption =
  <
    ValueKey extends string,
    LabelKey extends string | ((item: ObjectIn) => OptionLabel),
    ObjectIn extends Record<ExtractStrings<ValueKey | LabelKey>, OptionValue>
  >(
    valueKey: ValueKey,
    labelKey: LabelKey,
    objIn: ObjectIn | null | undefined
  ): Option<string, string> | null  => {
    return null // The specific details of the implementation are not crucial here
  };

Now, let the curry functions handle the heavy lifting for you!

const toOptionCurried = curry3r(toOption);
const toOptionStringCat = toOptionCurried((params) => `${params.x} (${params.y})`)
const toOptionString = toOptionCurried("y")

const myObj = { x: "foo", y: "bar" } as const 

const result = toOptionStringCat("x")(myObj);
const result2 = toOptionString("x")(myObj)
 

Answer â„–2

It is correct to say that direct inference becomes impossible, at least based on the limitations of currying. Several examples illustrate why this is so, or you may proceed directly to the solution.

Take note of the following:

const myObj = { x: "foo", y: "bar" } as const 
const result2 = toOption("x", "z")(myObj)

An error exists here! The issue lies with myObj, which reads as follows:

Argument of type '{ readonly x: "foo"; readonly y: "bar"; }' 
is not assignable to parameter of type 'Record<"x" | "z", string>'.

The error pertains to myObj, where it's not a matter of z being incompatible but rather myObj not fitting the object structure of {x: string, z: string}. Similarly, consider the following function

const result = toOption("x", (params) => `${params.x} (${params.y})`)(myObj)

There is no way to restrict params to myObj. What if someone uses

toOption("x", (params) => "")
without running the curry function? How would we validate params?

Solutions

I have proposed several potential resolutions, depending on your specific scenario/preference for adopting these strategies.

Switch the order of the curried functions

I would be comfortable with a version that requires me to explicitly specify the ObjectIn type when...

If that's the case, why not simply pass in the ObjectIn from the start! I strongly believe that your type model should align with your data, hence reversing the curry order! Something like this...

export const toOption2 =
  <
    ObjectIn extends Record<string, OptionValue>
  >(
    objectIn: ObjectIn
  ) => (valueKey: keyof ObjectIn, labelKey: keyof ObjectIn | ((p: ObjectIn) => string)): Option<string, string> => {
    return null!
  };

const result1 = toOption2(foobar)("foo", (params) => `${params.foo} ${params.baz}`)
const result2 = toOption2(foobar)("foo", 'baz')

View on Typescript Playground

If you still want to maintain old shapes, function overloading can be used to support this. It even supports associative currying, although this complicates implementation and has typing support limitations.

function toOptionSolution<
    ValueKey extends keyof ObjectIn,
    LabelKey extends keyof ObjectIn,
    ObjectIn extends Record<ExtractStrings<ValueKey | LabelKey>, OptionValue>
  >(
    valueKeyOrObjIn: ObjectIn,
    labelKey?: undefined
  ): (valueKey: ValueKey, labelKey: LabelKey | ((p: ObjectIn) => string) ) => void
function toOptionSolution<
    ValueKey extends keyof ObjectIn,
    LabelKey extends keyof ObjectIn,
    ObjectIn extends Record<ExtractStrings<ValueKey | LabelKey>, OptionValue>
  >(
    valueKeyOrObjIn: keyof ObjectIn,
    labelKey: keyof ObjectIn | ((p: ObjectIn) => string | null)
  ): (objIn: ObjectIn) => string | null
function toOptionSolution
  <
    ValueKey extends string,
    LabelKey extends string,
    ObjectIn extends Record<ExtractStrings<ValueKey | LabelKey>, OptionValue>
  >(
    valueKeyOrObjIn: ValueKey | ObjectIn,
    labelKeyOrObjIn: LabelKey | ObjectIn,
  ): any
  
  {
    return null!
  };

const foobar = {foo: 'bar', baz: 'bang'} as const
const notworking1 = toOptionSolution(foobar)("ababa", 'bababa')
const notworking2 = toOptionSolution(foobar)("foo", 'bababa')
const notworking3 = toOptionSolution('foo', 'bang')(foobar)
const working1 = toOptionSolution(foobar)("foo", (params) => `${params.foo} ${params.baz}`)
const working2 = toOptionSolution(foobar)("foo", 'baz')

View on Typescript Playground

Create a curry class

This approach differs slightly, taking inspiration from how Java handles currying... An example in JavaScript is how Jest operates

expect(sum(1, 2)).toBe(3);

Since a class can be generic, we can apply the ObjectIn type to the class, allowing all derived functions to utilize that type. This also enables a form of partial type inference. Simply supply the ObjectIn type to the class to store it, then use the toOption method for our currying needs.

const foobar = {foo: 'bar', baz: 'bang'} as const

class ToOption<ObjectIn extends Record<string, string>> {
  // No real properties...
  constructor() {}

  toOption =
  <
    ValueKey extends keyof ObjectIn,
    LabelKey extends keyof ObjectIn | ((item: ObjectIn) => string)
  >(
    valueKey: ValueKey,
    labelKey: LabelKey,
  ) => (objectIn: ObjectIn | null | undefined): string | null => {
    return null!
  }
}

const result = new ToOption<typeof foobar>().toOption('foo', (params) => `${params.foo} ${params.bang}`)(foobar)
const result2 = new ToOption<typeof foobar>().toOption('foo', 'bang')(foobar)

View on TS Playground

As an alternative, it might be beneficial to transition entirely to more OOP-style code if pursuing this path. Alternatively, consider these suggestions:

const foobarc = ToOption<typeof foobar>()
// Multiple methods?
const result = foobarc.toOption('foo', 'baz').takeObject(foobar)
// Pass key/labels in constructor?
const result2 = ToOption<typeof foobar>('foo', 'bar').takeObject(foobar)

The class can also extend the Function class to enable callability.

const foobar = {foo: 'bar', baz: 'bang'} as const

// https://stackoverflow.com/a/40878674/17954209
class ExFunc extends Function {
  [x: string]: any
  constructor() {
    super('...args', 'return this.__self__.__call__(...args)')
    var self = this.bind(this)
    this.__self__ = self
    return self
  }
}

interface ToOptionCompact<ObjectIn extends Record<string, string>> {
  (objectIn: ObjectIn): string | null
}
class ToOptionCompact<ObjectIn extends Record<string, string>> extends ExFunc {
  // No real properties...
  constructor(valueKey: keyof ObjectIn, labelKey: keyof ObjectIn) {
    super()
  }
  
  __call__ = (objectIn: ObjectIn | null | undefined): string | null => {
    return null!
  }
}

const result1 = new ToOptionCompact<typeof foobar>("foo", "baz")({} as {not: 'foobar'})
const result2 = new ToOptionCompact<typeof foobar>("foo", "baz")(foobar)

View on TS Playground

Explicitly pass type parameters

Last resort - unfortunately, this necessitates specifying all three parameters since partial type inferencing isn't supported (reference: Proposal: Partial Type Argument Inference #26242). While left as a straightforward exercise, this option may prove cumbersome, requiring three parameters in your case, along with a potentially tricky inline annotation for function types.

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

Angular 8 ngFor not displaying expected information from object

I have a set of data which includes IDs, versions, and user details for Paul and Peter. See below: var data = { id: 1 version: 1 user: [ { name: 'paul' }, { name: 'peter' ...

Grouping elements of an array of objects in JavaScript

I've been struggling to categorize elements with similar values in the array for quite some time, but I seem to be stuck Array: list = [ {id: "0", created_at: "foo1", value: "35"}, {id: "1", created_at: "foo1", value: "26"}, {id: "2", cr ...

Creating an Object Type from a String Union Type in TypeScript

How can I go about implementing this? type ActionNames = 'init' | 'reset'; type UnionToObj<U> = {/* SOLUTION NEEDED HERE */} type Result = UnionToObj<ActionNames>; // Expected type for Result: `{ init: any, reset: any }` ...

When the key property is utilized, the state in react useState is automatically updated; however, for updating without it, the useEffect or a

I am working on a component that looks like this: import React, { useState } from "react"; import { FormControl, TextField } from "@material-ui/core"; interface IProps { text?: string; id: number; onValueChange: (text: stri ...

Error in Typescript stating that the property 'children' is not found on the imported interface of type 'IntrinsicAttributes & Props'

When I try to import an interface into my Card component and extend CardProps, a yarn build (Typescript 4.5.4) displays the following error: Type error: Type '{ children: Element[]; className: string; border: true; disabled: boolean; }' is not as ...

Receiving an error in Typescript when passing an object dynamically to a React component

Encountering a typescript error while attempting to pass dynamic values to a React component: Error message: Property 'title' does not exist on type 'string'.ts(2339) import { useTranslation } from "react-i18next"; import ...

What is the best way to access the "document" in a vscode webview provider?

I am currently working on developing an extension for vscode that utilizes React for the interface. However, I have encountered an issue where I am unable to insert my react root component into the HTML of the webview. Despite trying various approaches, n ...

The expected React component's generic type was 0 arguments, however, it received 1 argument

type TCommonField = { label?: string, dataKey?: string, required?: boolean, loading?: boolean, placeholder?: string, getListOptionsPromissoryCallback?: unknown, listingPromissoryOptions?: unknown, renderOption?: unknown, getOptionLabelFor ...

Changing Image to Different File Type Using Angular

In my Angular Typescript project, I am currently working on modifying a method that uploads an image using the input element of type file. However, I no longer have an input element and instead have the image file stored in the assets folder of the project ...

There is no overload that matches this call in Next.js/Typescript

I encountered a type error stating that no overload matches this call. Overload 1 of 3, '(props: PolymorphicComponentProps<"web", FastOmit<Omit<AnchorHTMLAttributes, keyof InternalLinkProps> & InternalLinkProps & { ...; ...

Is it feasible to implement early-return typeguards in Typescript?

There are instances where I find myself needing to perform type checks on variables within a function before proceeding further. Personally, I try to minimize nesting in my code and often utilize early-return statements to keep the main functionality of a ...

Do Not Activate the Android App with Deeplink in Ionic3

I'm currently using the ionic-plugin-deeplinks to enable deep linking within my app. Here are the steps I followed: $ ionic cordova plugin add ionic-plugin-deeplinks --variable URL_SCHEME=myapp --variable DEEPLINK_SCHEME=https --variable DEEPLINK_HOS ...

The functionality of React setState seems to be malfunctioning when activated

Having encountered an unusual issue with react's setState() method. Currently, I am utilizing Azure DevOps extensions components and have a panel with an onClick event that is intended to change the state of IsUserAddedOrUpdated and trigger the addOr ...

Is there a way to create a TypeScript function that can accept both mutable and immutable arrays as arguments?

Writing the following method became quite complicated for me. The challenge arose because any method receiving the result from catchUndefinedList now needs to handle both mutable and immutable arrays. Could someone offer some assistance? /** * Catch any ...

Iterating over a JSON array using *ngFor

Here is the JSON structure that I have: { "first_name": "Peter", "surname": "Parker", "adresses": { "adress": [{ "info1": "intern", "info2": "bla1" }, { "info1": "extern", "info2": "bla2" }, { "info1": " ...

An issue occurred with building deployments on Vercel due to a typing error

When attempting to deploy my build on Vercel, I encountered an error. The deployment works fine locally, but when trying to build it on vercel, the following error occurs: [![Type error: Type '(ref: HTMLDivElement | null) => HTMLDivElement | null&a ...

You must add the module-alias/register to each file in order to use path aliases in

I am currently utilizing typescript v3.6.4 and have the following snippet in my tsconfig.json: "compilerOptions": { "moduleResolution": "node", "baseUrl": "./src", "paths": { "@config/*": ["config/*"], "@config": ["config"], ...

Customizing dropzone color while dragging an image in Angular

I have been using Angular and created an image input field. Within this input field, I implemented a dropzone that allows users to drag files into it. Is there a way for me to modify the background of the dropzone when a file is being dragged into it? Thi ...

Searching for MongoDB / Mongoose - Using FindOneById with specific conditions to match the value of an array inside an object nestled within another array

Although the title of this question may be lengthy, I trust you grasp my meaning with an example. This represents my MongoDB structure: { "_id":{ "$oid":"62408e6bec1c0f7a413c093a" }, "visitors":[ { "firstSource":"12 ...

Mastering the Implementation of Timetable.js in Angular with TypeScript

I am currently working on integrating an amazing JavaScript plugin called Timetable.js into my Angular6 project. You can find the plugin here and its repository on Github here. While searching for a way to implement this plugin, I stumbled upon a helpful ...