Identify a variety of potential types within an array passed using the spread operator

I'm on a quest to deduce the type for every spread argument of my type function.

Suppose I have two fields defined as follows.

type Field<T> = { value: T, name: string }

const field1 = { value: 12, name: 'age' }
const field2 = { value: 'foo', name: 'nickname' }

and I wish to pass these fields as spread arguments to the following function, which would be invoked like this

useForm('registration', field1, field2)

My attempt involved using conditional type inference based on the official documentation, which did address the issue to some extent

type InferredFields<T> = T extends { value: infer V }[]
  ? Record<string, Field<V>>
  : never

const useForm = <T extends Field<unknown>[]>(name: string, ...args: T) => {
  const fields: InferredFields<T> = args.reduce(
    (res, field) => ({
      ...res,
      [field.name]: field.value,
    }),
    {} as InferredFields<T>,
  )
  return {
    name,
    fields
  }
}

const form = useForm('bar', field1, field2)

The only problem I encountered is the difficulty in discriminating the union generated by the inferred value of the passed array generic based on the specific value being used.

type FieldValue<T> = T extends { value: infer V } ? V : never

// The issue arises from the return type of form being
// { fields: Record<string, string | number> } 
// instead of accurately inferring the value type
const v1: FieldValue<typeof field1> = form.fields['age'].value // error
const v2: FieldValue<typeof field2> = form.fields['nickname'].value // error

https://i.sstatic.net/SnW4K.png

Any suggestions on how to correctly map the value types for each Field type passed as an argument?

Answer №1

Okay, so here's a bit of a complex concept that involves type recursion.

What I've done is created the following type:

export type FormFields<T extends readonly unknown[]> = T extends readonly [
  infer FieldType,
  ...infer Rest,
]
  ? FieldType extends { value: infer V }
    ? { [key: string]: Field<V> } & FormFields<Rest>
    : never
  : never

It may seem confusing at first glance but becomes clearer when put into practice. Let me break it down for you.

We start by passing a generic T in the shape of unknown[]. We extract the first value (type) FieldType from the array and the remaining parameters Rest.

We then analyze the object and infer its value

V</code as another generic. This allows us to shape our object as type <code>Field<V></code, where we assign the value type <code>V
to our type Field from earlier. We then intersect this with a recursive call to out FormFields type in the form of FormField<Rest>, which handles the remaining arguments in a First-In-First-Out style recursive algorithm.

This type can now be utilized in our useForm function.

const useForm = <T extends Array<Field<unknown>>>(
   name: string
   ...addedFields: T
) => addedFields.reduce(
 (fields, field) => ({
   ...fields,
   [field.name]: field,
 }),
 {} as FormFields<T>
) as FormFields<T>

There's a slight limitation if you try to assign it to a constant like so:

const fields: FormFields<T>

You'll encounter a type error stating that Field<unknown> is not assignable to FormFields<T>. This constraint exists because of the specified T in the useForm function, which requires T to be an

Array<Field<unknown>>
. The FormFields type's generic
T</code must be instantiated as <code>unknown[]
, otherwise the inferred type won't match the constraint of
Field</code. On the other hand, <code>useForm
needs a constraint to
Array<Field<unknown>>
to prevent users from passing any array as arguments. So, it's somewhat of a compromise, but using the final assertion with as suffices as it maintains the desired object structure with all necessary types.

The result is correct types derived from the recursive inference:

form.fields['age'] // Field<12>
form.fields['nickname'] // Field<'foo'>

// Additional information below (not essential for the answer):

// If you need to convert Field<'foo'> to Field<string>, for example for an onChange handler that cannot accept "as const" values, a simple helper makes the transformation easy.
export type InferredToPrimitive<T> = T extends string
  ? string
  : T extends number
  ? number
  : T extends boolean
  ? boolean
  : T extends Record<string, unknown>
  ? Record<string, unknown>
  : T extends Array<unknown>
  ? Array<unknown>
  : T

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

Modify the key within an array of objects that share a common key

I have an object structured as follows: NewObjName: Object { OLDCOLUMNNAME1: "NEWCOLUMN_NAME1", OLDCOLUMNNAME2: "NEWCOLUMN_NAME2", OLDCOLUMNNAME3: "NEWCOLUMN_NAME3"} Next, there is an array containing objects in this format: ...

Absolute imports in create-react-app do not function properly when using yarn v2 workspaces alongside typescript

I am currently utilizing yarn v2 workspaces, and within my workspaces, I have a frontend project built using create-react-app / react-scripts. My goal is to enable absolute imports in the frontend application so that I can simply do things like import Butt ...

Unveiling the Ultimate Method to Package Angular 2 Application using SystemJS and SystemJS-Builder

I'm currently in the process of developing an application and I am faced with a challenge of optimizing the performance of Angular 2 by improving the loading speed of all the scripts. However, I have encountered an error that is hindering my progress: ...

How come the splice method is changing the value of the original object?

There's something strange happening with this code I'm trying out. Code: const x = [{ a: 'alpha', b: 'beta' }, { a: 'gamma' }]; const y = x[0]; y.a = 'delta'; x.splice(1, 0, y) console.log(x) Output: [ ...

What is the best way to create TypeScript declarations for both commonjs modules and global variables?

Wanting to make my TypeScript project compatible with both the commonjs module system and globals without modules. I'm considering using webpack for bundling and publishing it into the global namespace, but running into issues with the definitions (.d ...

Move the cache folder for NextJS to a new location

Is it possible to customize the location of the cache folder currently located in "./.next/cache"? I am interested in modifying this because I am developing an application that receives high traffic daily, and I plan to deploy multiple applications from m ...

Learn how to bring a component into another component within Angular

I have developed a component named CopySchedulefromSiteComponent and now I am looking to import it into another component called SiteScheduleComponent. However, I am unsure of the correct way to do this. The CopySchedulefromSiteComponent contains one fiel ...

I possess both a minimum and maximum number; how can I effectively create an array containing n random numbers within

Given a minimum number of 10.5 and a maximum number of 29.75, the task is to generate an array within these two ranges with a specific length denoted by 'n'. While the function for generating the array is provided below, it is important to calcul ...

Wondering how to implement HubSpot Conversations SDK in a Typescript/Angular application?

Recently, I came across some useful javascript code on this website window.HubSpotConversations.widget.load(); window.HubSpotConversations.widget.refresh(); window.HubSpotConversations.widget.open(); window.HubSpotConversations.widget.close(); Now, I am l ...

The properties required by the type for typescript reactjs are not present

I've come across an array with the following structure: export const SideBarTags = [ { name: 'Tutorials', link: '../tutorials', icon: faFileAlt, dropdownItems: null, active: false, }, { name: 'An ...

Using Karma-Jasmine to Import Spy without anyImplicitAny

If I include the configuration setting noImplicitAny in the tsconfig.json file of my Angular 4+ project: "noImplicitAny": true, ...and then try to import and use Spy in a unit test: import { Spy } from "karma-jasmine"; I encounter this console error wh ...

What is the best way to add all the items from an array to a div element?

I am currently facing an issue where only the last object in my array is being added to my div using the code below. How can I modify it to add all objects from the array to my div? ajaxHelper.processRequest((response: Array<Vehicle.Vehicle>) ...

Is there a way to mock a "find" call in mockingoose without getting back "undefined"?

I am currently working with mockingoose 2.13.2 and mongoose 5.12.2, leveraging Typescript and jest for testing purposes. Within my test scenario, I am attempting to mock a call to my schema's find method. Here is what I have tried: import mockingoose ...

The disappearance of the "Event" Twitter Widget in the HTML inspector occurs when customized styles are applied

Currently, I am customizing the default Twitter widget that can be embedded on a website. While successfully injecting styles and making it work perfectly, I recently discovered that after injecting my styles, clicking on a Tweet no longer opens it in a ne ...

Typescript encounters difficulty locating the Express module

My venture into creating my debut NodeJS application has hit a roadblock. Following advice from multiple blogs, I have been attempting to build my first nodejs app in typescript by following the steps below: npm install -g express-generator npm install - ...

Unable to resolve external modules in TypeScript when using node.js

I wanted to integrate moment.js into my node application, so I proceeded by installing it using npm: npm install <a href="/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail="adc0c2c0c8c3d9ed9f8399839d">[email protected]</a> J ...

Tips for locating the index of a substring within a string with varying line endings using Typescript

I am faced with the task of comparing two strings together. abc\r\ndef c\nde My goal is to determine the index of string 2 within string 1. Using the indexOf() method is not an option due to different line endings, so I require an altern ...

Lazy loading in Angular allows you to navigate directly to a certain feature component

I'm currently working on implementing lazy loading in Angular 6, and I want to include links on my main homepage that direct users to specific feature components. Here is the hierarchy of my project: app.module.ts |__homepage.component.ts |__options ...

Is there a way to bring in both a variable and a type from a single file in Typescript?

I have some interfaces and an enum being exported in my implementation file. // types/user.ts export enum LoginStatus { Initial = 0, Authorized = 1, NotAuthorized = 2, } export interface UserState { name: string; loginStatus: LoginStatus; }; ex ...

The function purported by WEBPACK_MODULE_13___default(...) does not exist

Scenario : I've been working on a small library (let's call it myLibrary) using TypeScript and Webpack. Everything seemed to be running smoothly until I imported the library into a React application, which resulted in a crash. On the Library Sid ...