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

What is the best way to extract values from case-sensitive query param variables?

I am dealing with a URL that contains the query string id. However, the variable id can appear as either 'id' or 'Id' in the URL. From my understanding, these two variations will be treated differently. To handle URLs like the followin ...

retrieve the router information from a location other than the router-outlet

I have set up my layout as shown below. I would like to have my components (each being a separate route) displayed inside mat-card-content. The issue arises when I try to dynamically change mat-card-title, as it is not within the router-outlet and does not ...

Do you have an index.d.ts file available for canonical-json?

I am currently working on creating an index.d.ts file specifically for canonical-json. Below is my attempted code: declare module 'canonical-json' { export function stringify(s: any): string; } I have also experimented with the following sn ...

Error message: The ofType method from Angular Redux was not found

Recently, I came across an old tutorial on Redux-Firebase-Angular Authentication. In the tutorial, there is a confusing function that caught my attention: The code snippet in question involves importing Actions from @ngrx/effects and other dependencies to ...

Modifying elements in an array using iteration in typescript

I'm trying to figure out how to iterate over an array in TypeScript and modify the iterator if necessary. The TypeScript logic I have so far looks like this: for (let list_item of list) { if (list_item matches condition) { modify(list_ite ...

How can you display or list the props of a React component alongside its documentation on the same page using TypeDoc?

/** * Definition of properties for the Component */ export interface ComponentProps { /** * Name of something */ name: string, /** * Action that occurs when component is clicked */ onClick: () => void } /** * @category Componen ...

What is the best way to utilize TypeScript module augmentation with material-ui components?

I have gone through the answers provided in this source and also here in this link, but it appears that they are outdated. I attempted to enhance the type definition for the button component in various ways, including a separate typings file (.d.ts) as we ...

The lazy loading feature in Angular 12 is not functioning correctly for path modules

My application has a jobs module with various components, and I'm trying to lazy load it. However, I've encountered an issue where accessing the module through the full path http://localhost:4200/job/artist doesn't work, but accessing it thr ...

What properties are missing from Three.js Object3D - isMesh, Material, and Geometry?

Currently, I am working with three.js version r97 and Angular 7. While I can successfully run and serve the application locally, I encounter type errors when attempting to build for production. Error TS2339: Property 'isMesh' does not exist on ...

Tips on how to effectively unit test error scenarios when creating a DOM element using Angular

I designed a feature to insert a canonical tag. Here is the code for the feature: createLinkForCanonicalURL(tagData) { try { if (!tagData) { return; } const link: HTMLLinkElement = this.dom.createElement('link'); ...

Transferring Information Between Components

After logging into my login component, I want to pass data to my navbar component but unfortunately, my navbar content does not update. The navbar component is located in the app-module while the login component is in a separate module. I attempted to us ...

What could be the reason behind TypeScript ignoring a variable's data type?

After declaring a typed variable to hold data fetched from a service, I encountered an issue where the returned data did not match the specified type of the variable. Surprisingly, the variable still accepted the mismatched data. My code snippet is as fol ...

How can I search multiple columns in Supabase using JavaScript for full text search functionality?

I've experimented with various symbols in an attempt to separate columns, such as ||, |, &&, and & with different spacing variations. For example .textSearch("username, title, description", "..."); .textSearch("username|title|description", "..."); U ...

Is TypeScript the new replacement for Angular?

After completing a comprehensive tutorial on Angular 1.x, I decided to explore Angular 2 on angular.io. However, upon browsing the site, I noticed references to Typescript, Javascript, and Dart. This left me wondering if my knowledge of Angular 1.x is now ...

Even with the use of setTimeout, Angular 5 fails to recognize changes during a lengthy operation

Currently, I am facing an issue with displaying a ngx-bootstrap progress bar while my application is loading data from a csv file. The Challenge: The user interface becomes unresponsive until the entire operation is completed To address this problem, I a ...

Using `this` within an object declaration

I am encountering an issue with the following code snippet: const myObj = { reply(text: string, options?: Bot.SendMessageOptions) { return bot.sendMessage(msg.chat.id, text, { reply_to_message_id: msg.message_id, ...options }) ...

After integrating session store into my application, nestjs-sequelize is not synchronizing with any models

I'm currently working on developing a Discord bot along with a website dashboard to complement it. Everything is running smoothly, except for the backend Nestjs API that I am in the process of creating. I decided to use Sequelize as the database for m ...

Problem with TypeScript involving parameter destructuring and null coalescing

When using parameter destructuring with null coalescing in TypeScript, there seems to be an issue with the optional name attribute. I prefer not to modify the original code like this: const name = data?.resource?.name ?? [] just to appease TypeScript. How ...

Retrieve data from a URL using Angular 6's HTTP POST method

UPDATE: Replaced res.json(data) with res.send(data) I am currently working on a web application using Angular 6 and NodeJS. My goal is to download a file through an HTTP POST request. The process involves sending a request to the server by calling a func ...

Pass the parameter name to the controller using the Change function in Angular 2

When creating a string from multiple inputs, I have a requirement to include the name of the input element as the second parameter in a function. <input [(ngModel)]="programSearched" name="programSearched"(ngModelChange)="stringBuilderOnChangeMaker(pro ...