Using selectors and mappers in Typescript generics

I am looking to create a versatile selector and mapper method.

interface State {
    user: {
        name: string;
        age: number;
    }
}

const pickName = (state: State) => state.user.name;
const selectAge = (state: State) => state.user.age;

const currentState: State = { user: { name: 'John Doe', age: 38 } };

My goal is to develop a generic TypeScript function that can achieve the desired outcome without relying on any or object.

const customSelector = constructSelector(pickName, selectAge, (name, age) => `${name} - ${age}`);
  1. The constructSelector function should be able to handle ...args of selectors with the format (state: State) => and return any value.
  2. The final argument will be a mapper function that automatically understands the types it expects.

Answer №1

Let's discuss the implementation of buildSelector():

function buildSelector<T extends any[], U>(...args: [
  ...selectors: { [I in keyof T]: (state: State) => T[I] },
  callback: (...args: T) => U
]): (state: State) => U {
  const selectors = args.slice(0, -1) as ((state: State) => any)[];
  const callback = args.at(-1) as (...args: any) => U;
  return (state) => callback(...selectors.map(f => f(state)))
}

The function signature might seem convoluted due to the need to accept multiple selectors before a single callback function. This setup resembles using a rest parameter with an additional parameter afterward. However, JavaScript restricts placing the rest parameter at the end of the parameter list. If we envisioned a hypothetical scenario where JavaScript allowed leading rest elements, the function structure could be represented like so:

// This is neither valid TypeScript nor valid JavaScript, do not use:
function buildSelector<T extends any[], U>(
  ...selectors: { [I in keyof T]: (state: State) => T[I] },
  callback: (...args: T) => U
): (state: State) => U {
  return (state) => callback(...selectors.map(f => f(state)))
}

In the theoretical case above, the function would work straightforwardly, accepting state and invoking the callback on a map operation on selectors. Nonetheless, such logic isn't directly transferrable to real-world scenarios owing to complexities addressed by type assertions.


Transitioning back to reality:

function buildSelector<T extends any[], U>(...args: [
  ...selectors: { [I in keyof T]: (state: State) => T[I] },
  callback: (...args: T) => U
]): (state: State) => U {
  const selectors = args.slice(0, -1) as ((state: State) => any)[];
  const callback = args.at(-1) as (...args: any) => U;
  return (state) => callback(...selectors.map(f => f(state)))
}

Due to variable-length arguments with the variable part at the start, the function necessitates a lone args rest parameter in the beginning. Despite JavaScript lacking support for a lead rest parameter, TypeScript features a leading rest tuple element, accommodating this design. Consequently, args becomes a variadic tuple containing selectors followed by a final callback element, mirroring previous specifications.

To disassemble args within the function body, conventional destructuring assignment fails due to Javascript limitations. Thus, methods like slice() and at() come into play. The implementation flexibility remains open to personal preference.

Type assertions and usage of the any type form a crucial part of ensuring loose enough types for selectors and callback, preventing compiler errors during

return (state) => callback(...selectors.map(f => f(state)))
. These operations compensate for the intricacies involved when manipulating tuple arrays and represent compromises given TypeScript constraints.


Evaluating the practical application:

const selector = buildSelector(
  selectName, selectAge, (name, age) => `${name} - ${age}`
);

const x = selector(state);
// const x: string
console.log(x) // "John Doe - 38" 

Successful inference by the compiler contextualizes name and age parameters within the final callback as string and number correspondingly. Likewise, understanding selector as (state: State)=>string based on the callback return type showcases operational coherence.

Note that actual implementation gets validated empirically amidst the compilation intricacies faced. The iterative testing phase validates overarching functionality.

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

Enabling state persistence in NextJS version 13 across multiple pages

Just getting started with NextJS and noticed that the old method of persisting components/state using _app.js is deprecated in NextJS 13. The new routing model allows for a layout.js file to house common components. However, I'm encountering an issue ...

The number entered will be incorporated into the API URL key value by passing the variable from page.html to services.ts

Recently diving into the world of Ionic, Angular, and Typescript, I've had a burning question. Can the number inputted be added to the API URL as one of the key values? I came across this helpful guide, specifically focusing on key event filtering (wi ...

Retrieving the necessary data from my object to perform a sum calculation in angular

Having trouble retrieving an attribute from an array in my code. In my .ts file, I am fetching data from my backend endpoint like this: export class PostFeedComponent implements OnInit { data: any = {}; constructor(private http: HttpClient) { t ...

Discover how TypeScript's strictNullChecks feature can help you identify null values with ease in your functions

Since Javascript often requires me to check if a value is `!= null && != ''`, I decided to create a function that checks for empty values: const isEmpty = (variable: any, allowEmptyString?: boolean): boolean => { return variable == null | ...

How to disable typescript eslint notifications in the terminal for .js and .jsx files within a create-react-app project using VS Code

I'm currently in the process of transitioning from JavaScript to TypeScript within my create-react-app project. I am facing an issue where new ESLint TypeScript warnings are being flagged for my old .js and .jsx files, which is something I want to avo ...

Issue during Docker build: npm WARN EBADENGINE Detected unsupported engine version

Currently, I am constructing an image based on mcr.microsoft.com/devcontainers/python:0-3.11-bullseye. In my docker file, I have included the following commands towards the end: RUN apt-get update && apt-get install -y nodejs npm RUN npm install np ...

Tips for automatically creating a categoryId using ObjectId for a category in Nest JS, Typescript, and Mongoose

book.entity.ts import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose'; import mongoose, { Document } from 'mongoose'; import { Category } from 'src/category/entities/category.entity'; export type BookDocument = Book & ...

Troubles with input handling in Angular

I've been diving into Traversy Media's Angular crash course recently. However, I've hit a roadblock that I just can't seem to get past. The problem arises when trying to style the button using a specific method. Every time I save and pa ...

What is the best way to access values from dynamically added components in Svelte when using data from a REST API in a loop?

Previously, I posted this query but now I've made changes to utilize a REST service for retrieving the item list. Everything functions as expected when the data is hardcoded, however, I encounter undefined values when using data from the REST service. ...

Determine the generic parameter of the output type by analyzing the resolved value of a data type within the function

I am looking to automatically determine the generic parameter of the return type by utilizing the resolved value of a type within the function. Consider the following: export type Context = any; export type Handler<T> = (ctx: Context) => Promise& ...

Optimizing performance in React: A guide to utilizing the Context and useCallback API in child

Utilizing the Context API, I am fetching categories from an API to be used across multiple components. Given this requirement, it makes sense to leverage context for managing this data. In one of the child components, the categories can be expanded using ...

What are the steps for personalizing themes in the Monaco editor?

I'm currently working on a code editor with Monaco. The syntax highlighting in Monaco for Javascript and Typescript only highlights keywords as dark blue, strings as brown, and numbers as light greenish-yellow. My goal is to customize the vs-dark the ...

Exploring TypeScript's Index Types: Introduction to Enforcing Constraints on T[K]

In typescript, we can utilize index types to perform operations on specific properties: interface Sample { title: string; creationDate: Date; } function manipulateProperty<T, K extends keyof T>(obj: T, propName: K): void { obj[propName] ...

Guide to utilizing services in Angular 2

As I've developed a service with numerous variables and functions, my goal is to inject this service into multiple components. Each component should have the ability to update certain variables within the service so that all variables are updated once ...

Issue arises when isomorphic-dompurify is used alongside dompurify in Next.js 13 causing compatibility problems

I am currently facing a compatibility problem involving isomorphic-dompurify and dompurify in my Next.js 13 project. It appears that both libraries are incompatible due to their dependencies on canvas, and I am struggling to find a suitable alternative. M ...

"Adjusting the size of a circle to zero in a D3 SVG using Angular 2

Trying to create a basic line graph using d3 in Angular 2 typescript. Below is the code snippet: import { Component, ViewChild, ElementRef, Input, OnInit } from '@angular/core'; import * as d3 from 'd3'; @Component({ selector: 'm ...

Preventing the compilation process from overwriting process.env variables in webpack: A step-by-step guide

Scenario I am currently working on developing AWS Lambda functions and compiling the code using webpack. After reading various articles, I have come to know that the process.env variables get automatically replaced during compilation. While this feature ...

What is the best way to transform a JS const into TSX within a Next.js application?

Exploring the code in a captivating project on GitHub has been quite an adventure. The project, located at https://github.com/MaximeHeckel/linear-vaporwave-react-three-fiber, showcases a 3D next.js application that enables 3D rendering and animation of mes ...

Loop through the coefficients of a polynomial created from a string using JavaScript

Summary: Seeking a method to generate a polynomial from specified coordinates, enabling coefficient iteration and optional point evaluation I am currently developing a basic JavaScript/TypeScript algorithm for KZG commitments, which involves multiplying c ...

Express is encountering an issue where it is unable to interpret the property 'name' of an undefined element coming from

I am attempting to create a fullstack application using Node.js and Angular with Material UI. I have encountered an issue while working on my web resource management application. Currently, I am facing an error during the registration and data submission ...