Get the callback function from a TypeScript dictionary using an argument in a higher-order function

In my coding dilemma, I encountered an issue with a function designed to work with callbacks and input objects. The initial function matches the input object's "type" property with a key in the callbacks object and calls the corresponding callback. Now, I'm striving to create a higher-order function that takes the callbacks object as input and returns a function for handling matching input objects. However, upon calling the returned function, TypeScript throws an error, claiming the input object is not assignable to a never type.

type Actor <K extends string, T> = (input: { type: K } & T) => void
type Actors <K extends string, T> = { [Key in K]: Actor<K, T> }
type TfromA<K extends string, A extends Actors<K, any>> =
  A extends Actors<K, infer T> ? T : never

export function marion<
  K extends string,
  A extends Actors<K, TfromA<K, A>>,
> (
  actors: A,
  input: { type: K } & TfromA<K, A>
): void {
  const actor = actors[input.type]
  actor(input)
}

interface Alpha { type: 'alpha', count: number }
interface Beta { type: 'beta', label: string }
const alphaBeta = {
  alpha: (input: Alpha) => console.log(input.count),
  beta: (input: Beta) => console.log(input.label)
}
const alpha: Alpha = { type: 'alpha', count: 42 }
marion(alphaBeta, alpha) // No errors

function higher<
  K extends string,
  A extends Actors<K, TfromA<K, A>>,
> (
  actors: A
): (input: TfromA<K, A> & { type: K }) => void {
  return function (input: TfromA<K, A> & { type: K }): void {
    marion(actors, input)
  }
}
const lower = higher(alphaBeta)
lower(alpha)
// Argument of type 'Alpha' is not assignable to parameter of type 'never'.
// The intersection 'Alpha & Beta & { type: string; }' was reduced to 'never' because property 'type' has conflicting types in some constituents.ts(2345)

I am seeking a solution to create a flexible and reusable function capable of handling various sets of callbacks and input objects while facilitating the creation of higher order functions effortlessly.

Check out the Playground: https://tsplay.dev/w2844m

Answer №1

Upon examining the code provided in the question, several issues come to light. To address these issues, let's explore a modified version of the code that is functional and compare it with the original:

type Input<K, V> = { type: K } & V
type Actor<K, V> = (input: Input<K, V>) => void
type Actors<T extends object> =
  { [K in keyof T]: Actor<K, T[K]> }

export function marion<T extends object, K extends keyof T>(
  actors: Actors<T>, input: Input<K, T[K]>
): void {
  const actor = actors[input.type]
  actor(input)
}

function higher<T extends object>(
  actors: Actors<T>
): <K extends keyof T>(input: Input<K, T[K]>) => void {
  return input => {
    marion(actors, input)
  }
}

To streamline readability, a type alias Input<K, V> has been introduced for {type: K} & V, a type that recurs multiple times throughout the code. The definition of the Actor<K, V> type remains largely unchanged, while the generic nature of the Actors mapped type provides flexibility based on the object type being transformed (source).

The function marion() now reflects this generality by accepting arguments specific to object type T and key K. TypeScript inference capabilities allow for automatic determination of T from the actors parameter typed as Actors<T>, eliminating the need for an explicit TfromA utility type. The invocation actors[input.type](input) dynamically resolves types at runtime.

Subsequently, the implementation of the higher() function—essentially a curried version of

marion()</code—is straightforward. It takes <code>actors
of type Actors<T> as input, necessitating only a generic declaration for T. The returned function introduces the type parameter K.

Operations proceed as expected when employing examples, as demonstrated below:

marion(alphaBeta, alpha); // successful execution
marion(gammaDelta, gamma); // successfully executed
const lower = higher(alphaBeta); // operational success
lower(alpha) // process completed without errors

Reasons contributing to the failure of your initial implementation:

  • The

    Actors<K extends string, T>
    structure proves cumbersome since every property is constrained to
    Actor<K, T></code, even if <code>K
    comprises a union of keys. Adopting a mapped type strategy over all properties of
    T</code facilitates handling of <code>K
    variabilities within the given context.

  • The intricacies associated with TfromA<K, A> complicate TypeScript's generic reasoning processes. Defined as a conditional type, accessibility to the actual nature of

    TfromA<K, A></code becomes ambiguous internally within functions like <code>marion
    and higher. TypeScript's innate ability to infer T directly from
    A</code renders this complexity redundant.</p>
    </li>
    <li><p>Your <code>higher()
    function exhibits generic declarations for K and A, yet fails to accommodate inputs corresponding to K. By exclusively accepting an A-typed input and enforcing constraints related to K, TypeScript encounters challenges in inferring type specifics. With inadequate support for constraint-based type argument deductions (microsoft/TypeScript#7234), attempts to resolve K devolve into defaulting to string, leading to unproductive outcomes.

Link to Code Playground

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

Mapping a response object to a Type interface with multiple Type Interfaces in Angular 7: A step-by-step guide

Here is the interface structure I am working with: export interface ObjLookup { owner?: IObjOwner; contacts?: IOwnerContacts[]; location?: IOwnerLocation; } This includes the following interfaces as well: export interface IObjOwner { las ...

Testing a React component that uses useParams: A step-by-step guide

I've been working on creating a BBS App using TypeScript, React, React Router, and React Testing Library. However, I've encountered an issue where a component utilizing useParams is not passing a test. Interestingly, it seems to be working correc ...

The implementation of Symbol.species in the Node.js Buffer class to generate a RapidBuffer seems illogical and confusing

While exploring the source code of ws, a popular WebSocket implementation for Node.js, I stumbled upon this specific piece of code: const FastBuffer = Buffer[Symbol.species]; But what exactly is this FastBuffer used for? Surprisingly, it seems that they a ...

Retrieving User's Theme Preference from Local Storage in Next.js Instantly

As mentioned in various other responses, such as this one, Next.js operates on both the client and server side, requiring a guard to properly fetch from localStorage: if (typeof localStorage !== "undefined") { return localStorage.getItem("theme") } else ...

Choose a value, then multiply it by a number using a reactive

I have been attempting to multiply a fixed value by the selected value of a mat-select element, for example A x B, where A remains constant and does not change while B is the changing value from the mat-select. After performing this multiplication, I aim ...

Issue: unable to establish a connection to server at localhost port 5000 while using Next.js getServerSideProps function

I am experiencing an issue with connecting to an API on localhost:5000. The API works perfectly when called from Postman or the browser, but it does not work when called inside Next.js getserverside props: mport { useEffect,useState } from "react"; i ...

Typescript's Intersection Types: The Key to Overlapping Properties

Looking to create a type-safe utility function in Typescript 4.0 for comparing properties of two objects, my initial code snippet is below: export function propertiesMatch<O extends object, T extends O, S extends O>(first: T, second: S, props: (keyof ...

Developing a specialized command-line application for currency conversion is my current project

Currently, I am working on developing a command-line application for currency exchange. I have created an interface to define the structure of an object array that will store the keys and values of currency names along with their current values in the inte ...

What is the correct way to set up Typescript for usage with Angular 2 in Visual Studio 2015?

I recently created an Angular 2 app using Visual Studio 2015 and TypeScript. I made sure to install TypeScript globally using the npm command "npm install -g [email protected]." However, when I try to build the project, I encounter several errors re ...

Issue encountered with passport-local in TypeScript: Unable to utilize 'new' with an expression that does not have a call or construct signature

Currently, I am attempting to implement the passport-local package in TypeScript (version 2.0.0RC); however, a compiler error has arisen: Error TS2351: It is not possible to use 'new' with an expression lacking a call or construct signature. ...

The Relationship between Field and Parameter Types in TypeScript

I am currently working on a versatile component that allows for the creation of tables based on column configurations. Each row in the table is represented by a specific data model: export interface Record { attribute1: string, attribute2: { subAt ...

What could be the reason for ngOnChanges lifecycle hook not getting invoked?

I am currently experimenting with Angular 2 in an effort to learn more about it. I noticed that ngOnChanges is not triggering in the code below: app.component.ts: import { Component, Input } from "@angular/core" import { FormsModule } from '@angular ...

Implementing Styled API in TypeScript with props: A Comprehensive Guide

I'm currently working on styling a component using the new styled API, not to be confused with StyleComponents. const FixedWidthCell = styled(TableCell)((props: { width: number }) => ({ width: props.width || 20, textAlign: "center", })) The i ...

Setting up tsconfig.json to enable support for either string literals or string templates involves adjusting the compiler options

After utilizing swagger codgen with the typescript-aurelia template to create API code, I noticed that a significant amount of string literals were being used in the resulting code. Despite encountering errors when running the transpiler tsc from the comma ...

Unable to translate text on the loading page

Encountering a peculiar issue with the translate service. Here's how I set it up: export class AppComponent implements OnInit { constructor( private translateService: TranslateService, angulartics2GoogleAnalytics: Angulartics2GoogleAnalytics ...

Ensuring type safety at runtime in TypeScript

While delving into the concept of type safety in Typescript, I encountered an interesting scenario involving the following function: function test(x: number){ console.log(typeof x); } When calling this method as test('1'), a compile time er ...

How can I call a method from a class using Typescript when getting an error saying that the property does not exist on the

Below is a service definition: export class MyService { doSomething(callbacks: { onSuccess: (data: Object) => any, onError: (err: any) => any }) { // Function performs an action } } This service is utilized in a component as shown be ...

Ways to implement logging in an NPM package without the need for a specific logging library

Currently, I am in the process of developing a company npm package using TypeScript and transferring existing code to it. Within the existing code, there are instances of console.log, console.warn, and console.error statements, as shown below: try { c ...

Unleashing the Power of Typescript and SolidJS: Expanding the Properties of JSX Elements

Is there a way to enhance the props of an existing JSX element in SolidJS and craft a custom interface similar to the ButtonProps interface shown in this React example below? import Solid from 'solid-js'; interface ButtonProps extends Solid.Butt ...

What steps should I take to establish a one-to-one relationship with legacy tables?

As I work on developing a web application (angular, nestjs, typeorm), I am faced with the challenge of linking two legacy user tables together within our existing system. Despite my efforts, I continue to encounter an error message related to column refere ...