Is it possible for Typescript to automatically infer object keys based on the value of a previous argument?

Currently, my goal is to create a translation service that includes type checking for both tags and their corresponding placeholders. I have a TagList object that outlines the available tags along with a list of required placeholders for each translated string.

export const TagList = {
    username_not_found: ['username']
};

In this setup, the key represents the tag name while the value consists of placeholders necessary in the translated text.

An example dictionary may look like this:

// Please note that the keys should be either numbers or strings, not like this unconventional format
const en: {[name: keyof (typeof TagList)]: string} = {
    "username_not_found": "The user {username} does not exist"
}

The method used for translating tags functions as follows:

this.trans("username_not_found", {username: "<a href="/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail="6d1e0200080203082d08150c00270123242017110704462b2725">[email protected]</a>"});

My objective is to enable type checking and autocompletion within my IDE for placeholder objects to ensure all placeholders are correctly configured.

For instance:

// Incorrect: Missing "username" placeholder.
this.trans("username_not_found", {});

// Also incorrect: Using a nonexistent placeholder "foobar".
this.trans("username_not_found", {foobar: "42"});

// Correct:
this.trans("username_not_found", {username: "<a href="/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail="295a46444c464748694e534a46515d474e05535452"></a>[email protected]"});

At present, I am utilizing keyof (typeof TagList) as the argument type for tagName. Although it currently works, I am seeking a way to deduce the structure of the second argument based on the first argument's value.

I aim to streamline the process by avoiding the need for multiple tag lists maintained simultaneously in various interfaces and objects.

Thank you for your assistance!

Answer №1

To start off, it is important to ensure that TagList remains immutable.

Afterwards, I proceeded to establish a literal type based on the key, resembling the functionality of Array.prototype.reduce

export const TagList = {
    username_not_found: ['username'],
    username_found: ['name'],
    batman: ['a', 'b']
} as const;

type Elem = string

type Reducer<
    Arr extends ReadonlyArray<Elem>, // array
    Result extends Record<string, any> = {} // accumulator
    > = Arr extends [] ? Result // if array is empty -> return Result
    : Arr extends readonly [infer H, ...infer Tail] // if array is not empty, do recursive call with array Tail and Result
    ? Tail extends ReadonlyArray<Elem>
    ? H extends Elem
    ? Reducer<Tail, Result & Record<H, string>>
    : never
    : never
    : never;

type TagList = typeof TagList;

const trans = <Tag extends keyof TagList, Props extends Reducer<TagList[Tag]>>(tag: Tag, props: Props) => null as any

trans("username_not_found", { username: "<a href="/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail="e89b87858d87868da88d90898598848dc68b8785">[email protected]</a>" }); // ok
trans("username_found", { name: "John" }); // ok
trans("batman", { a: "John", b: 'Doe' }); // ok

trans("username_not_found", { name: "<a href="/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail="97e4f8faf2f8f9f2d7f2eff6fae7fbf2b9f4f8fa">[email protected]</a>" }); // expected error
trans("username_found", { username: "John" }); // expected error

The main objective here is to transform the tuple ['username'] into { username: string }

If you were to achieve this in pure js, how would you go about it?

['username'].reduce((acc, elem) => ({ ...acc, [elem]: 'string' }), {})

I have employed a similar algorithm but opted for recursion over iteration.

This serves as a javascript equivalent of the Reducer utility type

const reducer = (arr: ReadonlyArray<Elem>, result: Record<string, any> = {}): Record<string, any> => {
    if (arr.length === 0) {
        return result
    }

    const [head, ...tail] = arr;

    return reducer(tail, { ...result, [head]: 'string' })
}

For additional insights, feel free to visit my blog

Answer №2

After creating a demo, I became intrigued to experiment with it. It appears to be functioning correctly for my needs.

const SomeStrings = {
  first: 'some text begin {test} {best}',
  second: ' {nokey} {key} some text in end',
  third: 'gsdfgsdfg',
} as const;

type ExtractReplacedParams<T extends string = string> = T extends `${infer _start}{${infer ks}}${infer _next}`
  ? _next extends `${infer _altStart}{${infer altKey}}${infer _altNext}`
    ? ExtractReplacedParams<_next> & { [k in ks]: string }
    : { [k in ks]: string }
  : Record<string, never>;

type Keys = keyof typeof SomeStrings;

type stMap = { [K in keyof typeof SomeStrings]: typeof SomeStrings[K] };

export const someTranslate = <K extends Keys>(key: K, replaceObj: ExtractReplacedParams<stMap[K]>): string => {
  const phrase = SomeStrings[key];

  if (replaceObj) {
    for (const [replaceKey, value] of Object.entries(replaceObj)) {
      phrase.replace(replaceKey, value);
    }
  }

  return phrase;
};

console.log(someTranslate('second', { nokey: 'seems', key: 'works' }));

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

Limit the parameter of a TypeScript method decorator based on the method's type

Is it possible to generically restrict the argument of a method decorator based on the type of the method it is applied to? I attempted to obtain the method's type that the decorator is applied to using TypedPropertyDescriptor<Method>. However, ...

When checking for a `null` value, the JSON property of Enum type does not respond in

Within my Angular application, I have a straightforward enum known as AlertType. One of the properties in an API response object is of this enum type. Here is an example: export class ScanAlertModel { public alertId: string; public alertType: Aler ...

What is the method to access an interface or type alias that has not been explicitly exported in TypeScript type definitions?

I am looking to create a new class that inherits from Vinyl. The constructor in the superclass takes a single parameter of type ConstructorOptions. export default class MarkupVinylFile extends Vinyl { public constructor(options: ConstructorOptions) { ...

Looking for a solution to the TypeScript & Mantine issue of DateValue not being assignable?

The required project dependencies for this task are outlined below: "dependencies": { "@mantine/core": "^7.6.2", "@mantine/dates": "^7.6.2", "@mantine/form": "^7.6.2", &q ...

How to immediately set focus on a form control in Angular Material without needing a click event

Currently working with Angular 9 and Material, we have implemented a Stepper to assist users in navigating through our workflow steps. Our goal is to enable users to go through these steps using only the keyboard, without relying on mouse clicks for contro ...

Determine characteristics of object given to class constructor (typescript)

My current scenario involves the following code: abstract class A { obj; constructor(obj:{[index:string]:number}) { this.obj = obj; } } class B extends A { constructor() { super({i:0}) } method() { //I wan ...

The practice of following the UpperCamelCase convention post object transformation

I encountered a situation where I have an object that returned the result from an RxJs subscribe method: result: any { message: null role: Object success: true } To better manage this object in TypeScript, I decided to convert it to a type ca ...

What is the process of transforming two forms into components and then integrating those components into a view in Angular 5?

Currently, I have two forms running smoothly on the same component as shown in InfoAndQualificationComponent.ts import { Component, OnInit } from '@angular/core'; import { FormGroup, FormControl } from "@angular/forms"; @Component({ selector: ...

JavaScript - Imported function yields varied outcome from module

I have a utility function in my codebase that helps parse URL query parameters, and it is located within my `utils` package. Here is the code snippet for the function: export function urlQueryParamParser(params: URLSearchParams) { const output:any = {}; ...

In React Typescript, the input type="checkbox" does not show any value in the value attribute

I'm facing an issue with displaying text next to a checkbox in React Typescript. When I try to use the value attribute, it doesn't seem to work as expected. Also, attempting to set innerHTML throws an error stating that input is a void element ta ...

Bring in d3 along with d3-force-attract

Recently, I have installed D3 along with d3-force-attract. npm install @types/d3 -S npm install -S d3-force-attract I am currently facing an issue with importing d3 force attract as it is not recognized as a typescript module, unlike d3 itself. The inco ...

Encountering a TypeScript error when using Redux dispatch action, specifically stating `Property does not exist on type`

In my code, there is a redux-thunk action implemented as follows: import { Action } from "redux"; import { ThunkAction as ReduxThunkAction } from "redux-thunk"; import { IState } from "./store"; type TThunkAction = ReduxThunk ...

Choosing a single item from multiple elements in React using React and typescript

In this particular project, React, TypeScript, and ant design have been utilized. Within a specific section of the project, only one box out of three options should be selected. Despite implementing useState and toggle functionalities, all boxes end up bei ...

Using Regular Expressions: Ensuring that a specific character is immediately followed by one or multiple digits

Check out the regular expression I created: ^[0-9\(\)\*\+\/\-\sd]*$ This regex is designed to match patterns such as: '2d6', '(3d6) + 3', and more. However, it also mistakenly matches: '3d&apos ...

Tips for refreshing the apollo cache

I have been pondering why updating data within the Apollo Client cache seems more challenging compared to similar libraries such as react-query. For instance, when dealing with a query involving pagination (offset and limit) and receiving an array of item ...

Ways to trigger the keyup function on a textbox for each dynamically generated form in Angular8

When dynamically generating a form, I bind the calculateBCT function to a textbox like this: <input matInput type="text" (keyup)="calculateBCT($event)" formControlName="avgBCT">, and display the result in another textbox ...

Encountering an error in Angular 8 where attempting to access an element in ngOnInit results in "Cannot read property 'focus' of null"

My html code in modal-login.component.html includes the following: <input placeholder="Password" id="password" type="password" formControlName="password" class="form-input" #loginFormPassword /> In m ...

Encountering a problem with the 'string' parameter when using TypeScript

I keep encountering the following error message: Element implicitly has an 'any' type because expression of type 'string' can't be used to index type '{ barkingRoadProject: string[]; }'. No index signature with a paramet ...

What could be causing my data to undergo alterations when transitioning from a frontend form submission to a backend API?

In my experience with Next.js 13 and Prisma, I encountered a peculiar issue. I had set up a basic form to collect user information for an api request. Oddly enough, when I printed the data right before sending it, everything seemed fine. However, upon arri ...

Reviewing for the presence of "Undefined" in the conditional statement within Transpiled Javascript code for

While perusing through some transpiled Angular code, I came across this snippet: var __decorate = (undefined && undefined.__decorate) || function (decorators, target, key, desc) { I'm puzzled by the usage of undefined in this context. Can an ...