Is it possible to implement typed metaprogramming in TypeScript?

I am in the process of developing a function that takes multiple keys and values as input and should return an object with those keys and their corresponding values. The value types should match the ones provided when calling the function.

Currently, the code is functional, but the type checking is not precise.

I attempted to use the Factory approach, hoping that TypeScript could infer the types for me.

Below is the factory code snippet. You can also check it out on the playground here.

const maker = (subModules?: Record<string, unknown>) => {
  const add = <Name extends string, Q>(key: Name, value: Q) => {
    const obj = {[key]: value};
    return maker({
      ...subModules,
      ...obj
    })
  }
  const build = () => {
    return subModules
  }

  return {
    add,
    build
  }
}

const m2 = maker()
  .add('fn', ({a, b}: { a: number, b: number }) => a + b)
  .add('foo', 1)
  .add('bar', 'aaaa')
  .build()
// m2.foo -> 1
// m2.bar -> 'aaaa'
// m2.fn({a: 1, b: 2}) -> 3
m2

Another option available is using pipeline(playground) which may be simpler:


type I = <T extends any[]>(...obj: T) => { [P in T[number]['key']]:  T[number]['value'] }
const metaMaker: I = <T extends any[]>(...subModules: T) => {
  return subModules.reduce((acc, curr) => {
    const op = {[curr.key]: curr.value}
    return {
      ...acc,
      ...op
    }
  }, {}) as { [P in T[number]['key']]: T[number]['value'] }
}
const m = metaMaker(
  {key: 'fn', value: ({a, b}: { a: number, b: number }) => a + b},
  {key: 'foo', value: 1},
  {key: 'bar', value: 'aaaa'},
)
// m.foo -> 1 
// m.bar -> 'aaaa'
// m.fn({a: 1, b: 2}) -> 3
// m

Answer №1

Just like the solution provided by @yeahwhat, this code has an added feature in the build function. When dealing with multiple records and returning their intersection, things can quickly become messy. The extends infer O part helps to simplify the intersection into a single type.

const creator = <Submodules extends Record<string, unknown> = {}>(subModules?: Submodules) => {
  const add = <Name extends string, Q>(key: Name, value: Q) => {
    const obj = {[key]: value};
    return creator<Submodules & Record<Name, Q>>({
      ...subModules,
      ...obj
    } as Submodules & Record<Name, Q>)
  }
  
  const build = () => {
    return subModules as Submodules extends infer O ? { [K in keyof O]: O[K] } : never;
  }

  return {
    add,
    build
  }
}

An additional option is also available:

type Narrow<T> =
    | (T extends infer U ? U : never)
    | Extract<T, number | string | boolean | bigint | symbol | null | undefined | []>
    | ([T] extends [[]] ? [] : { [K in keyof T]: Narrow<T[K]> });

const metaCreator = <T extends { key: string; value: any }[]>(...subModules: Narrow<T>) => {
  return (subModules as T).reduce((acc, curr) => {
    const op = {[curr.key]: curr.value}
    return {
      ...acc,
      ...op
    }
  }, {}) as { [P in T[number]['key']]: Extract<T[number], { key: P }>['value'] }
}

Your original solution was almost there, but I have incorporated a special type to refine the input type without needing to use as const. The missing piece was using

Extract<T[number], { key: P }>
to only retrieve the specific value for that key, instead of including all values for each key.

Check out the Playground (contains both versions)

Answer №2

To maintain the original type, you can use a generic T and merge it with a Record<Name, Q> each time a new entry is added, utilizing an intersection type as illustrated in this example (playground):

const creator = <T extends Record<string, any>>(subItems?: T) => {
  
  const append = <Name extends string, Q>(key: Name, value: Q) => {
    const obj = {[key]: value} as Record<Name, Q>;
    return creator<T & Record<Name, Q>>({...subItems, ...obj} as T & Record<Name, Q>)
  }

  const generate = () => {
    return subItems
  }

  return {
    append,
    generate
  }
}

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

Error encountered when an Angular expression is changed after it has already been checked in a dynamically generated component

I am encountering a problem with Angular and the CdkPortal/CdkPortalHost from @angular/cdk. I developed a service that allows me to associate a CdkPortalHost with a specified name and set its Component at any given time. This is how the service is struc ...

A guide to implementing angularjs app.service and $q in typescript

I am fairly new to TypeScript and AngularJS and I am struggling to find the correct answer for my issue. Below is the relevant code snippet: export class SidenavController { static $inject = ['$scope', '$mdSidenav']; constructor(p ...

What is the best way to extract the ID from an event in TypeScript?

HTML Code: <ion-checkbox color="dark" checked="false" id="1on" (ionChange)="onTap($event)" ></ion-checkbox> TypeScript Code: onTap(e) { console.log(e); console.log(e.checked); } I am trying to retrieve the id of the checkbox. H ...

TypeScript code runs smoothly on local environment, however encounters issues when deployed to production

<div> <div style="text-align:center"> <button class="btnClass">{{ submitButtonCaption }}</button> <button type="button" style="margin-left:15px;" class="cancelButton" (click)="clearSearch()"> {{ clearButtonCapt ...

Difficulty encountered when attempting to invoke a public function that makes use of a private function in TypeScript

I'm relatively new to TypeScript and I've been trying to replicate a pattern I used in JavaScript where I would expose functions through a single object within a module (like "services"). Despite my efforts, I'm facing some issues when attem ...

Encountering an issue with Angular 12 where a TypeError is being thrown, specifically stating "Cannot read properties of null (reading 'length') at

I encountered an error message while making a http request in my Angular Service. Strangely, this error only occurs after I logout, but it disappears upon logging back in: Below is the code snippet of my authentication Service: import { Injectable } from ...

Using variables within the useEffect hook in ReactJS

I am currently working on a project using Reactjs with Nextjs. I am facing an issue where I need to retrieve the value of "Editor" and alert it inside the handleSubmit function. Can anyone help me with how to achieve this? Here is my code snippet, any as ...

Having trouble configuring custom SCSS Vuetify variables with Vue 3, Vite, Typescript, and Vuetify 3

Having some trouble with custom variables.scss in Vuetify. Looking to make changes to current settings and added my code on stackblitz. Check it out here Been going through Vuetify documentation but can't seem to pinpoint the issue. Any assistance w ...

Can you tell me the data type of a Babel plugin parameter specified in TypeScript?

Struggling to find ample examples or documentation on writing a Babel plugin in TypeScript. Currently, I am working on a visitor plugin with the following signature: export default function myPlugin({ types: t }: typeof babel): PluginObj { In order to obt ...

Absence of "Go to Definition" option in the VSCode menu

I'm currently working on a Typescript/Javascript project in VSCODE. Previously, I could hover my mouse over a method to see its function definition and use `cmd + click` to go to the definition. However, for some unknown reason, the "Go to Definition" ...

Nested interfaces can utilize sum types

An example showcasing the use of sum types: interface Cash { amount: number, type: 'cash' } interface Card { amount: number, type: 'card', cardNumber: string } type Payment = Cash | Card const displayPayment = (payment: Pay ...

Identify the classification of unfamiliar items

Occasionally, you may find yourself in situations where you have to work with packages that were not designed with TypeScript in mind. For instance, I am currently using the two.js package in a React project with TypeScript strict mode enabled. It has been ...

Troubleshooting issue with Vue Class Component and Vuex-class causing ESLint error

I am interested in utilizing vuex-class to bind helpers for vuex and vue-class-component However, an error message is displayed: Error: Parsing error - Using the export keyword between a decorator and a class is not allowed. Please use `export @dec class ...

Generate a new perspective by incorporating two distinct arrays

I have two arrays containing class information. The first array includes classId and className: classes = [ {classid : 1 , classname:"class1"},{classid : 2 , classname:"class2"},{classid : 3 , classname:"class3"}] The secon ...

What is the best way to combine an array of objects into a single object in typescript?

Looking for some help with converting an array of objects into a single object using TypeScript. Here's the structure of the array: myArray = [ {itemOneKey: 'itemOneValue', itemTwoKey: 'itemTwoValue'}, {itemThreeKey: ' ...

Steps for making a "confirm" button within a modal that includes a redirect URL

I have developed a modal that, upon clicking on the confirm button, should redirect the user to the page titled securities-in-portfolio. modal <div class="modal-footer justify-content-center"> <button type="button" class ...

Reassigning Key Names and Types Based on Conditions

How can I modify object key names and properties in a way that allows existing keys and properties to remain the same or be modified (remapped)? My current approach does not properly handle mixed cases: export const FUNC_ENDING_HINT = "$func" as const; ty ...

Ways to show an object by comparing its object ID to the ID received from the server

I have a collection of objects structured as follows: defined in FruitModel.ts export interface ColorByFruit{ Id : number; name : string; color : string; } const Fruits: ColorByFruit[] = [ {Id:1, name:"Apple", color:&quo ...

Deriving a universal parameter from a function provided as an argument

My function can take in different adapters along with their optional options. // Query adapter type 1 type O1 = { opt: 1 } const adapter1 = (key: string, options?: O1) => 1 // Query adapter type 2 type O2 = { opt: 2 } const adapter2 = (key: string, opti ...

Issue encountered when attempting to import a module within the ionic framework

I encountered an issue in my project (built with the ionic-framework 3) where I included the following line to import the dialogflow module: const dialogflow = require('dialogflow'); However, when compiling, it resulted in the error message: ...