invoke a specified function at runtime

I recently came across a useful library called https://github.com/ivanhofer/typesafe-i18n

This library has the capability to generate strongly typed translation data and functions, as illustrated below. (the examples provided are simplified for clarity)

export type MyTranslations = {
  Hello: (arg: { field: unknown}) => string
  Bye: (arg: { field: unknown, date: unknown}) => string
  Foo: (arg: { field: unknown}) => unknown
  Bar: (arg: { max: unknown}) => unknown,
  test: string // this is just here to show that not every property of MyTranslations needs to be a function
}

const translations: MyTranslations = {
  Hello: (arg: { field: unknown}) => 'hello',
  Bye: (arg: { field: unknown, date: unknown}) => 'bye',
  Foo: (arg: { field: unknown}) => 'foo',
  Bar: (arg: { max: unknown}) => 'bar',
  test: '' // this is just here to show that not every property of MyTranslations needs to be a function
}

In my code, I have a function that dynamically translates messages without prior knowledge of what needs to be translated.
By leveraging TypeScript typing information, it can infer potential translations using keyof.
Here's a snippet of the code in progress.
I've invested significant time in this, and I'm not certain if it's achievable or practical, but I'm eager to find out :)

// preparation
interface MyParams {
  [index: string]: boolean | number | string | undefined
  field?: keyof MyTranslations
}

interface Result {
  transKey: keyof MyTranslations,
  params?: MyParams
}

const results: Result[] = [
  {
    transKey: 'Hello',
    params: {
      field: 'Bye'
    }
  },
  {
    transKey: 'Bar',
    params: {
      max: 'test'
    }
  }
] 

type PickByType<T, V> = {
  [P in keyof T as T[P] extends V | undefined ? P : never]: T[P]
}

The translation function

function translate(results: Result[]) {
  results.forEach((result: Result) => {
      type A = PickByType<MyTranslations, Function>
      type C = keyof A
     
      if(result.params) {
        type T = typeof result.params
        type Req = Required<T>

        const req = result.params as Req
        
        const func = translations[result.transKey]
        type F = typeof func
        

        const f = translations as A
        f[result.transKey as C](req)

      }
  })
}

translate(results)

The issue arises at f[result.transKey as C](req)

Error

Argument of type 'Required<MyParams>' is not assignable to parameter of type '{ field: unknown; } & { field: unknown; date: unknown; } & { field: unknown; } & { max: unknown; }'.
  Property 'date' is missing in type 'Required<MyParams>' but required in type '{ field: unknown; date: unknown; }'

This constraint makes sense. TypeScript expects an intersection type.
Therefore, I thought of a possible solution where I could create this type (holding all the required parameters field, max, and date) and utilize this information to construct a new object with corresponding properties, as depicted in pseudo code below

type D = getAllParametersFromTypeAsIntersectionType() // <- this is easy
const newParams = createNewParamsAsTypeD(result.params)

Any suggestions or ideas?

TS Playground

Answer №1

If you want to manipulate result.params in a different way, it's best not to consider it as an intersection type. In reality, it doesn't encompass all properties but only those required by translations[result.transKey] for a specific result.transKey. TypeScript assumes it should be an intersection due to the lack of clarity regarding the relationship between result.transKey and result.params. The current Result type fails to define such a connection; for instance, specifying

{ transKey: 'Hello', params: { max: 'Bye' } }
would still be valid despite being incorrect for Hello. Even if union types were employed for each transKey, challenges like "correlated unions" arise within the forEach() callback.

The absence of direct support for correlated unions is addressed in microsoft/TypeScript#30581. The suggested solution involves leveraging generics in a specific manner as outlined in microsoft/TypeScript#47109.

The concept revolves around creating a "base" object type that captures the key-value association of interest. Subsequent operations should rely on this type along with generic indexes into it and mapped types over it.

The foundational object type looks like this:

interface TransArg {
    Hello: { field: unknown; };
    Bye: { field: unknown; date: unknown; };
    Foo: { field: unknown; };
    Bar: { max: unknown; };
}

This can actually be derived from MyTranslations through:

type TransKey = {
  [K in keyof MyTranslations]: MyTranslations[K] extends (arg: any) => any ? K : never
}[keyof MyTranslations]

type TransArg = { [K in TransKey]: Parameters<MyTranslations[K]>[0] }

The focus shifts towards rewriting the type of translations in terms of TransArg:

const _translations: { [K in TransKey]: (arg: TransArg[K]) => void } =
  translations;

While seemingly straightforward, this verification ensures that translations adheres to the specified mapped type. By substituting _translations for translations, the compiler gains better insight into its behavior across arbitrary keys K.

A more precise definition of Result emerges as a distributive object type:

type Result<K extends TransKey = TransKey> =
  { [P in K]: { transKey: P, params?: TransArg[P] } }[K]

Result<K> aligns with the desired type for a particular K, while Result<TransKey> compiles the complete union of Result<K> for all K in TransKey. This default argument strategy clarifies Result usage and allows for:

const results: Result[] = [
  {
    transKey: 'Hello',
    params: {
      field: 'Bye'
    }
  },
  {
    transKey: 'Bar',
    params: {
      max: 'test'
    }
  }
]

Any attempt to mix up the params will trigger an error, ensuring data integrity.

Lastly, results.forEach() call necessitates a generic callback function:

function translate(results: Result[]) {
  results.forEach(<K extends TransKey>(result: Result<K>) => {
    if (result.params) _translations[result.transKey](result.params);
  })
}

Within the callback, interactions between _translations[result.transKey] and result.params epitomize their corresponding types, enabling seamless compilation.

Explore the code example in TypeScript 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

Can a decorator be added to a Typescript class after it has been created?

Is it possible to update a class with inversify's @injectable decorator after it has been created? My use case involves using a mocking library like ts-auto-mock to generate a mock for me, and then applying the @injectable decorator to bind the mock t ...

What is the best way to iterate through the result of an HTTP request in Angular 11?

I am a beginner with Angular and currently working in Angular 11. I am facing issues with making an http request. Despite going through numerous Stack Overflow posts, none of the solutions seem to work for me, even though some questions are similar to mine ...

Error Encountered When Searching for Modules in a Yeoman-Generated Express TypeScript Project

After generating an express typescript project using yeoman, I encountered some errors whenever I tried running the application. The errors stated that it could not find modules such as "morgan", "body-parser", and "cookie-parser". Even though these module ...

Guide to importing a function from a Javascript module without declaration

Currently, I am utilizing a third-party Javascript library that includes Typescript type declarations in the form of .d.ts files. Unfortunately, as is often the case, these type declarations are inaccurate. Specifically, they lack a crucial function which ...

Cloud Formation from CDK doesn't pause for addDependency to finish

I'm currently in the process of building a CDK stack and I am fairly new to CDK. My goal is to create a Simple Email Service (SES) ConfigurationSet followed by an EmailIdentity. The issue I encountered is that the creation of the EmailIdentity fails d ...

Instantiate the component array upon object instantiation

I'm currently in the process of learning Angular 2, so please bear with me if this question seems trivial. I am attempting to create a dynamic form that can be bound to a model. However, I am encountering an issue where I am unable to initialize my ar ...

Looking to personalize the MUI - datatable's toolbar and place the pagination at the top?

I successfully managed to hide the toolbar icon, but I am struggling with positioning pagination from bottom to top. Additionally, I am attempting to add two buttons (reset and apply) in the view-Column toolbar without any success in customizing the class. ...

Function in Typescript that accepts either a single object or an array of objects

We frequently use a simple function declaration where the function can accept either a single object or an array of objects of a certain type. The basic declaration looks like this: interface ISomeInterface { name: string; } class SomeClass { pu ...

What methods can I use to make sure the right side of my React form is properly aligned for a polished appearance?

Trying to create a React component with multiple input field tables, the challenge is aligning the right side of the table correctly. The issue lies in inconsistent alignment of content within the cells leading to disruption in overall layout. Experimente ...

Encountering issues with Socket.io: consistently experiencing websocket connection failures along with persistent 404 errors on the

I am facing issues with setting up a websocket using socket.io. The server-side seems to be making a GET call successfully, but on the client-side, I am getting a 404 error: GET http://localhost:6543/socket.io/?uuid=258c4ab9-b263-47ca-ab64-83fe99ea03d4& ...

Discover how to access JSON data using a string key in Angular 2

Trying to loop through JSON data in angular2 can be straightforward when the data is structured like this: {fileName: "XYZ"} You can simply use let data of datas to iterate over it. But things get tricky when your JSON data keys are in string format, li ...

Is there a more effective way to implement a Custom Validator using .forEach?

I have developed my own validation class as a learning exercise. Do you think this is an effective approach, or do you have suggestions for improvement? import { AbstractControl } from '@angular/forms'; export class ProjectNameValidator { pr ...

Issue with Material UI grid not rendering properly in TypeScript environment

I've been trying to replicate a grid from material-ui using React and Typescript. You can see a live demo here. I modified the example to work with Typescript, so my demo.tsx file looks like this: Code goes here... If you check out the live demo, y ...

Issue with importing MomentJS globally in TypeScript

When it comes to defining global external modules in TypeScript, there is a useful option available. For instance, if you have jQuery library loaded externally, you can set up a global definition without having to include its duplicate in the TypeScript bu ...

Error: The layout was unable to display the template body

I've been working on a web application with express and eta, but I'm running into an issue with including partials in my templates. Despite trying to include a file partial (refer to the Docs), the compiled template doesn't seem to incorpor ...

What could be causing axios to not function properly when used with async/await in this particular scenario

I need to update the DoorState when a button is clicked. After sending a request to the API to change the DoorState, I then call another API to check the status of the robot. Even though the DoorState has been successfully changed, it seems that the chan ...

The TypeScript class for Date has a property that outputs a string

In my TypeScript code, I have defined a model class as follows: export class Season { ID: number; Start: Date; } Below is an example of how this model class is utilized within a component: export class SeasonsComponent { seasons: Season[]; sele ...

In my current project, I am working with Knockout and TypeScript but I am encountering difficulties in firing the window-resize event

Instead of using jquery, I prefer working with a custom handler for the $(window).resize(function () { ... event. If there is a way to achieve this without relying on jquery, please feel free to share it in the comments below. The code snippet below show ...

Encountered error: Unable to locate module - Path 'fs' not found in '/home/bassam/throwaway/chakra-ts/node_modules/dotenv/lib' within newly generated Chakra application

Started by creating the app using yarn create react-app chakra-ts --template @chakra-ui/typescript. Next, added dotenv with yarn add dotenv Inserted the following code block into App.tsx as per the instructions from dotenv documentation: import * as dote ...

What is the process for applying cdkDropList to the tbody when using mat-table instead of a traditional HTML table?

I have been experimenting with the mat-table component in my Angular project, following a simple example from the documentation: <table mat-table [dataSource]="dataSource" class="mat-elevation-z8"> <!--- These columns can be ...