Disarrayed generic parameters in TypeScript

The title of the question may not be perfect, but it's the best I could do.

Imagine a scenario where there is a function that accepts a callback along with an optional array. The callback takes an index and another optional array as parameters, and then returns a new value. The function itself returns an accessor object that contains a method to execute the callback:

function createAccessor<T extends unknown[] | undefined, U>(
  indexfn: (index: number, arr?: T) => U,
  array?: T
) {
  return {
    valueAt(index: number) {
      return indexfn(index, array);
    },
  };
}

const x = createAccessor(() => 1);
const y = createAccessor((index) => index);
const z = createAccessor((index, v) => v![index], ["a", "b", "c"]); 
                                     // ^ Have to use non-null assertion operator here :( 
const w = createAccessor((index, v) => v![index]); // TypeScript should throw error here but doesn't

const x100 = x.valueAt(100); // 1
const y10 = y.valueAt(10); // 10
const z1 = z.valueAt(1); // "a"

If indexfn requires both index and array inputs, then the array parameter MUST be provided to createAccessor. However, if it only needs the index, the array can be left blank.

To avoid explicitly passing undefined by reversing the order of parameters like in

createAccessor(undefined, () => 1)
, do you have any alternative suggestions?

Answer №1

If you aim to achieve compiler-verified type safety, both in the caller's context and within the implementation itself, consider restructuring your code like so:

function createAccessor<R extends [array?: unknown[]], U>(
    indexfn: (index: number, ...r: R) => U,
    ...r: R
) {
    return {
        valueAt(index: number) {
            return indexfn(index, ...r);
        },
    };
}

Using a rest parameter of an optional tuple type for array instead of an optional parameter will enable the compiler to understand that array might be absent only if indexfn doesn't require any additional parameters after index. In your current version, both array and r are optional parameters, potentially leading to independent presence or absence.

By utilizing spread syntax with rest parameters for the final argument, we ensure clarity for the compiler on what is transpiring (replacing it with indexfn(index, r[0]) would prompt a compiler error due to lack of equivalence visibility).

Thus, this approach aligns with your intended behavior:

const x = createAccessor(() => 1); // valid
const y = createAccessor((index) => index); // valid
const z = createAccessor((index, v) => v[index], ["a", "b", "c"]); // valid
const w = createAccessor((index, v) => v[index]); // invalid!
const x100 = x.valueAt(100); // yields a number
const y10 = y.valueAt(10); // yields a number
const z1 = z.valueAt(1); // yields a string

Alternatively, if maintaining your current implementation (perhaps due to performance considerations involving the spread operator – though caution against premature optimization), but ensuring satisfaction for callers, one could implement overloads to differentiate between two types of function calls based on unique call signatures:

interface ValueAt<U> {
    valueAt(index: number): U;
}

// distinct call signatures
function createAccessor<U>(indexfn: (index: number) => U): ValueAt<U>;
function createAccessor<T extends unknown[], U>(
    indexfn: (index: number, arr: T) => U, array: T): ValueAt<U>;

// actual implementation
function createAccessor(indexfn: (index: number, arr?: any[]) => any, array?: any[]
) {
    return {
        valueAt(index: number) {
            return indexfn(index, array);
        },
    };
}

This approach mirrors the caller's perspective closely; either invoke the function with one argument demanding a single-parameter indexfn, or with two arguments necessitating a two-parameter indexfn. Thus, indexfn and array aren't independently optional:

const x = createAccessor(() => 1); // acceptable
const y = createAccessor((index) => index); // acceptable
const z = createAccessor((index, v) => v[index], ["a", "b", "c"]); // acceptable
const w = createAccessor((index, v) => v[index]); // error!
const x100 = x.valueAt(100); // yields a number
const y10 = y.valueAt(10); // yields a number
const z1 = z.valueAt(1); // yields a string

The implementation undergoes loose verification, hence altering return indexfn(index, array) to return "oopsiedoodle" wouldn't raise compiler complaints. Awareness of potential consequences is key in such scenarios.

Link to playground for code 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

Typing Redux Thunk with middleware in TypeScript: A comprehensive guide

I recently integrated Redux into my SPFx webpart (using TypeScript), and everything seems to be working fine. However, I'm struggling with typing the thunk functions and could use some guidance on how to properly type them. Here's an example of ...

Using Material-UI with TypeScript

Attempting to integrate TypeScript/React with Material UI has been quite the challenge for me so far. Here is my index.tsx file: declare function require(p: string): any; var injectTapEventPlugin = require("react-tap-event-plugin"); injectTapEventPlugin( ...

Enhancing Typescript Arrow Function Parameters using Decorators

Can decorators be used on parameters within an arrow function at this time? For instance: const func: Function = (@Decorator param: any) => { ... } or class SomeClass { public classProp: Function = (@Decorator param: any) => { ... } } Neither W ...

Exploring the fusion of different interfaces and props in React using typescript

I have designed an interface as shown below, representing the "base button". export interface ButtonProps { backgroundColor?: Colors, children?: React.ReactNode | JSX.Element, style?: CSSProperties, disabled?: boolean, onClick?: () => ...

Angular2 checkboxes for filtering data

I'm working with an Angular2 grid that showcases an array of Fabrics, each with its own color or fabric type properties. Right now, all Fabrics are displayed in the grid, but I need to implement a series of checkboxes for color and fabric type, along ...

Encountering Angular error when trying to assign an undefined array to another array while using mock data

Attempting to conduct unit testing on a component involving the retrieval of 'Groups' data through a data resolver class. Below is the corresponding code: export class GroupsComponent implements OnInit, OnDestroy { group: IGroup; groups: IGro ...

Exploring TypeScript and React: Redefining Type Definitions for Libraries

As I transition from JSX to TSX, a challenge has arisen: My use of a third-party library (React-Filepond) This library has multiple prop types The provided types for this library were created by an individual not affiliated with the original library (@ty ...

Unable to access or modify properties within a function passed as an argument

deleteDialog(item, func: Function) { this.dialogService .open(ConfirmDialogComponent, { context: { title:"Are you sure?", cancelClss: "info", confirmClss: "danger", }, ...

Changing setState in React does not update the current state

My challenge lies in altering the value of a TreeSelect component from the antd (antdesign) UI library. I followed their instructions outlined in their documentation, with the only divergence being the use of Typescript. The issue I encounter is with ch ...

Retrieving selected values from an ngx dropdown list

I am having trouble implementing ngx dropdown list in this way: <ngx-dropdown-list [items]="categoryItems" id="categoriesofdata" [multiSelection]="true" [placeHolder]="'Select categories'"></ngx-dropdown-list> ...

Stop extra properties from being added to the return type of a callback function in TypeScript

Imagine having an interface called Foo and a function named bar that accepts a callback returning a Foo. interface Foo { foo: string; } function bar(callback: () => Foo): Foo { return callback(); } Upon calling this function, if additional pr ...

Ways to customize the OverridableComponent interface within Material-UI

Is there a way to effectively use the Container component with styled-components incorporating ContainerProps, while still being able to pass the component prop that belongs to the OverridableComponent interface? Currently, I am encountering an error when ...

What is the best way to add a repository in Nest.js using dependency injection?

I am encountering an issue while working with NestJS and TypeORM. I am trying to call the get user API, but I keep receiving the following error message: TypeError: this.userRepository.findByIsMember is not a function. It seems like this error is occurring ...

How can we eliminate the need for specifying the order of generic arguments in TypeScript?

In the development of my middleware engine, I have incorporated various generic arguments that are specific to the particular implementation in use. export type Middleware< Store = never, Args = unknown, Response = unknown > = ( context: { ...

Nest.js: initializing properties from a superclass in a controller

I have a question about unit testing controllers in the Nest.js framework. My issue is that the property from a superclass is not initialized in the controller class when creating a test module. Here is an example of the code I am referring to: export cl ...

What is the best way to interrupt the current song playing?

I am currently working on developing an audio player using reactjs that has a design similar to this https://i.sstatic.net/Hnw0C.png. The song boxes are rendered within a map function, and when any song box is clicked, it should start playing. However, I a ...

The attribute 'size' is not recognized within the data type 'string[]' (error code ts2339)

When using my Windows machine with VSCode, React/NextJS, and Typescript, a cat unexpectedly hopped onto my laptop. Once the cat left, I encountered a strange issue with my Typescript code which was throwing errors related to array methods. Below is the co ...

Utilize style as a module in Angular

The issue at hand: Let's take a look at this project structure: /src /public /styles /general /tables.scss /secure /components /someTable1 /someTable.component.ts /someTable.component.css /someTa ...

`I'm having difficulty transferring the project to Typescript paths`

I posted a question earlier today about using paths in TypeScript. Now I'm attempting to apply this specific project utilizing that method. My first step is cloning it: git clone <a href="/cdn-cgi/l/email-protection" class="__cf_email__" data-cf ...

What is the process for implementing a new control value accessor?

I have a directive that already implements the ControlValueAccessor interface (directive's selector is input[type=date]) and now I need another directive that also implements ControlValueAccessor with the selector input[type=date][datepicker] - let&ap ...