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

What is the best method for compressing and decompressing JSON data using PHP?

Just to clarify, I am not attempting to compress in PHP but rather on the client side, and then decompress in PHP. My goal is to compress a JSON array that includes 5 base64 images and some text before sending it to my PHP API. I have experimented with l ...

Tips for extracting data from an Angular object using the *ngFor directive

https://i.stack.imgur.com/ai7g1.png The JSON structure displayed in the image above is what I am working with. My goal is to extract the value associated with the key name. This is the approach I have taken so far: <span *ngFor="let outlet of pr ...

An error in npm occurred: "The name "@types/handlebars" is invalid."

While attempting to install typedoc using npm, I encountered the following error: npm ERR! Invalid name: "@types/handlebars" To resolve this issue, I proceeded to install @types/handlebars directly by running: npm install @types/handlebars However, I r ...

Is there a method in TypeScript to create an extended type for the global window object using the typeof keyword?

Is it possible in TypeScript to define an extended type for a global object using the `typeof` keyword? Example 1: window.id = 1 interface window{ id: typeof window.id; } Example 2: Array.prototype.unique = function() { return [...new Set(this)] ...

Is Angular 9's default support for optional chaining in Internet Explorer possible without the need for polyfill (core-js) modifications with Typescript 3.8.3?

We are in the process of upgrading our system to angular 9.1.1, which now includes Typescript 3.8.3. The @angular-devkit/[email protected] utilizing [email protected]. We are interested in implementing the optional chaining feature in Typescript ...

One approach to enhance a function in Typescript involves encapsulating it within another function, while preserving

What I Desire? I aim to create a function called wrap() that will have the following functionality: const func = (x: string) => 'some string'; interface CustomObject { id: number; title: string; } const wrapped = wrap<CustomObject> ...

NextJS 13 causes tailwind to malfunction when route group is utilized

I've encountered an issue in my NextJS 13 application where Tailwind classes are no longer being applied after moving page.tsx/layout.tsx from the root directory to a (main) directory within the root. I suspect that there may be a configuration that i ...

Error: You can't use the 'await' keyword in this context

I encountered a strange issue while using a CLI that reads the capacitor.config.ts file. Every time the CLI reads the file, it throws a "ReferenceError: await is not defined" error. Interestingly, I faced a similar error with Vite in the past but cannot ...

Derived a key from an enum member to be used as an interface key

I am attempting to configure an object property by selecting its key using a computed key, via an enum, as shown in the code snippet below. Unfortunately, solutions 1 and 2 are not functioning. These are the solutions I need to implement in my code becaus ...

Struggling to retrieve the value of a text field in Angular with Typescript

In the Angular UI page, I have two types of requests that I need to fetch and pass to the app.component.ts file in order to make a REST client call through the HTML page. Request 1: Endpoint: (GET call) http://localhost:8081/api/products?productId=7e130 ...

Modify the ShortDate formatting from "2020/06/01" to "2020-06-01"

I am looking to modify the shortDate format in my template file. Currently, when I try to change it to use "-", it still displays the date with "/". What I actually want is for the output to be formatted as yyyy-MM-dd. <ngx-datatable-column name="C ...

How do I make functions from a specific namespace in a handwritten d.ts file accessible at the module root level?

Currently, I am working on a repository that consists entirely of JavaScript code but also includes handwritten type declarations (automerge/index.d.ts). The setup of the codebase includes a Frontend and a Backend, along with a public API that offers some ...

Issue with Ant Design form validation

After reading through the documentation, I attempted to implement the code provided: Here is a basic example: import { Button, Form, Input } from "antd"; export default function App() { const [form] = Form.useForm(); return ( <Form f ...

initiate an animated sequence upon the initialization of the Angular server

Is there a way to launch a Netflix animation after my server has started without success using setTimeout? I don't want to share the lengthy HTML and CSS code. You can view the code for the animation in question by visiting: https://codepen.io/claudi ...

Leveraging the power of React's callback ref in conjunction with a

I'm currently working on updating our Checkbox react component to support the indeterminate state while also making sure it properly forwards refs. The existing checkbox component already uses a callback ref internally to handle the indeterminate prop ...

Tips for passing multiple items for the onselect event in a ng-multiselect-dropdown

I've got a multi-select dropdown with a long list of options. Currently, when I choose a single item, it triggers the Onselect event and adds data from the newArrayAfterProjectFilter array to the myDataList based on certain conditions in the OnselectE ...

What is the function return type in a NextJS function?

In my project using NextJS 13, I've come across a layout.tsx file and found the following code snippet: export default function RootLayout({ children }: { children: React.ReactNode }) { return ( <html> <head /> <body&g ...

An issue has occurred: Uncaught (in promise): NullInjectorError: R3InjectorError(AppModule)[NavbarComponent -> NavbarComponent

I've been working on implementing Google Auth login with Firebase, but I keep encountering an issue when trying to load another component or page after logging in. I've spent the entire day trying to debug this problem and it's really frustr ...

Tips for simulating or monitoring an external function without an object using Typescript 2 and Jasmine 2 within an Angular 4 application

In order to verify if a function called haveBeenCalledWith() includes the necessary parameters, I am seeking to validate the correctness of my call without actually executing the real methods. I have experimented with multiple solutions sourced from vario ...

Steps to duplicate the package.json to the dist or build directory while executing the TypeScript compiler (tsc)

I am facing an issue with my TypeScript React component and the package.json file when transpiling it to es5 using tsc. The package.json file does not get copied automatically to the dist folder, as shown in the provided screenshot. I had to manually copy ...