Create a tuple type by mapping an object with generics

I have a specified object:

config: {
  someKey: someString
}

My goal is to generate a tuple type based on that configuration. Here is an example:

function createRouter<
  T extends Record<string, string>
>(config: T) {
  type Router = {
    // How can I access T[K] below? For instance:
    // ['someKey', typeof someString]
    navigate: [keyof T, T[K]];
  };
}

Live Example

To elaborate further, the reason I need this Router type is because I am passing it to a function that returns an object of functions whose parameters are based on the original configuration. The returned object looks like this:

{
  [P in keyof T]: (...params: Extract<T[P], readonly unknown[]>) => Promise<void>
}

In this case, T is the type being passed in as Router. A practical use of this setup would be:

const router = createRouter({
  user: 'user/:id'
});

router.navigate('user', { id: '123' });

In the above usage, navigate, user, and id are all automatically determined. The tuple [keyof T, T[K]] is utilized within the navigate function, where keyof T serves as the first parameter and T[K} as the second (note: I also transform T[K] to extract the :id parameter, but that's not relevant for this discussion).

If config contains multiple keys, the type should still be inferred correctly, as demonstrated below:

const router = createRouter({
  user: 'user/:postId',
  post: 'post/:postId'
});

router.navigate('user', { userId: '123' }); // no issue
router.navigate('post', { postId: '123' }); // no problem
router.navigate('post', { userId: '123' }); // error

Please note that I'm not seeking guidance on transforming 'user/:userId', as I already possess the necessary type conversion. My focus here is solely on having the config represented in tuple form.

Answer №1

To effectively define the configuration for the router, make sure to mark it as a constant so that the specifics can flow downwards. Then, break down the path by separating it with / and retain only the portions that contain :${param}. Next, create an object using these extracted keys.

type IsParameter<Part extends string> = Part extends `:${infer Anything}` ? Anything : never;
type FilteredParts<Path extends string> = Path extends `${infer PartA}/${infer PartB}`
  ? IsParameter<PartA> | FilteredParts<PartB>
  : IsParameter<Path>;

interface MyRouter<T extends Record<string, string>> {
  navigate<P extends keyof T>(
    path: P, 
    options: Record<FilteredParts<T[P]>, string>,
  ): void;
};

declare function createRouter<
  T extends Record<string, string>
>(config: T): MyRouter<T>;

const router = createRouter({
  user: 'user/:userId',
  post: 'post/:postId',
  userPosts: 'user/:userId/post/:postId',
} as const);

router.navigate('user', { userId: '123' }); // all good
router.navigate('post', { postId: '123' }); // all good
router.navigate('userPosts', { postId: '123' }); // error userId missing
router.navigate('post', { userId: '123' }); // error

Interactive Demo: link
Source:

Answer №2

I'm a little unsure of your current needs, but I'll do my best to provide some assistance.

To make the navigate function more generic and able to determine the correct parameters for the route, it's not feasible to spread a tuple type.

The code below should serve as a solid starting point:

type Router<Config extends Record<string, string>> = {
  navigate<Route extends keyof Config>(
    route: Route,
    params: ExtractParams<Config[Route]>
  ): Promise<void>;
};

type Infer<T> = { [K in keyof T]: T[K] };
function createRouter<Config extends Record<string, string>>(
  config: Infer<Config>
): Router<Config> {
  return {} as Router<Config>;
}

const router = createRouter({
  user: "user/:userId",
  post: "post/:postId",
} as const);

router.navigate("user", { userId: "1" });
router.navigate("post", { postId: "2" });

In addition, here is my parameter extraction method (without the optional parameters section):

/**
 * Compute is a helper converting intersections of objects into
 * flat, plain object types.
 *
 * @example
 * Compute<{ a: string } & { b: string }> -> { a: string, b: string }
 */
export type Compute<obj> = { [k in keyof obj]: obj[k] } & unknown;

type CreateParamObject<keys extends string> = {
  [k in keys]: string;
};

type ExtractParams<path extends string> = Compute<
  path extends `${infer start}/:${infer param}/${infer rest}`
    ? CreateParamObject<param> &
        ExtractParams<start> &
        ExtractParams<`/${rest}`>
    : path extends `${infer start}/:${infer param}`
    ? CreateParamObject<param> & ExtractParams<start>
    : unknown
>;

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

Tips for referencing functions in TypeScript code

In regard to the function getCards, how can a type be declared for the input of memoize? The input is a reference to a function. import memoize from "fast-memoize"; function getCards<T>( filterBy: string, CardsList: T[] = CardsJSON.map((item, i ...

Avoiding the insertion of duplicates in Angular 2 with Observables

My current issue involves a growing array each time I submit a post. It seems like the problem lies within the second observable where the user object gets updated with a new timestamp after each post submission. I have attempted to prevent duplicate entr ...

Angular 2+ Service for tracking application modifications and sending them to the server

Currently I am facing a challenge in my Angular 4 project regarding the implementation of the following functionality. The Process: Users interact with the application and it undergoes changes These modifications are stored locally using loca ...

The use of the .reset() function in typescript to clear form data may lead to unexpected

I've been trying to use document.getelementbyID().reset(); to reset form values, but I keep running into an error in TypeScript. Property 'reset' does not exist on type 'HTMLElement'. Here's how I implemented it: const resetB ...

find the element in cypress with multiple child elements

Looking for a way to target the first HTML element that contains more than 2 children. Another option is to access the children elements of the first parent element. <div class="market-template-2-columns"> <button type="button&q ...

Encountered a Next-Auth Error: Unable to fetch data, TypeError: fetch failed within

I've been struggling with a unique issue that I haven't found a solution for in any other forum. My Configuration NextJS: v13.0.3 NextAuth: v4.16.4 npm: v8.19.2 node: v18.12.1 When the Issue Arises This particular error only occurs in the pr ...

Utilizing ngModel within a nested ngFor loop in Angular to capture changing values dynamically

I have been working on developing a screen using Angular and I'm facing an issue with binding values using ngModel. https://i.sstatic.net/DCJ3T.png Here is my implementation. Any help would be appreciated. The model entity I am using for binding the ...

Utilize a list of Data Transfer Objects to populate a dynamic bar chart in recharts with

I received a BillingSummaryDTO object from a Rest API using Typescript: export interface BillingSummaryDTO { paid?: number, outstanding?: number, pastDue?: number, cancelled?: number, createdAt?: Moment | null, } export async function ...

Understanding and parsing JSON with object pointers

Is it possible to deserialize a JSON in typescript that contains references to objects already existing within it? For instance, consider a scenario where there is a grandparent "Papa" connected to two parents "Dad" and "Mom", who have two children togeth ...

Demonstrating a feature in a custom Angular Material dialog box

I have a reusable custom Material UI Dialog that I want to utilize to show different components. For instance, I would like to display a Login component on one occasion and a Registration component on another. However, the issue arises when I assign my com ...

What are the best ways to incorporate a theme into your ReactJS project?

Looking to create a context for applying dark or light themes in repositories without the need for any manual theme change buttons. The goal is to simply set the theme and leave it as is. Currently, I have a context setup like this: import { createContext ...

Verifying TypeScript Class Instances with Node Module Type Checking

My current task involves converting our Vanilla JS node modules to TypeScript. I have rewritten them as classes, added new functionality, created a legacy wrapper, and set up the corresponding Webpack configuration. However, I am facing an issue with singl ...

Extracting information from an object retrieved through an http.get response can be accomplished by utilizing various methods and

I am working with an API that returns a JSON object like this: { "triggerCount": { "ignition_state_off": 16, "ignition_state_on": 14, "exit_an_area": 12, "enter_an_area": 19, "door_unlocked": 1, "door_l ...

Creating a multipart/form-data POST request in Angular2 and performing validation on the input type File

I am working on sending an image (base64) via a POST request and waiting for the response. The POST request should have a Content-Type of multipart/form-data, and the image itself should be of type image/jpg. This is what the POST request should look like ...

`Is there a way to repurpose generic type?`

For instance, I have a STRING type that is used in both the test and test2 functions within the test function. My code looks like this: type STRING = string const test = <A = STRING>() => { test2<A>("0") } const test2 = <B& ...

I keep receiving error code TS2339, stating that property 'total' is not recognized within type any[]

Check out this code snippet. Can you provide some assistance? responseArray: any[] = []; proResponseArray: any[] = []; clearArray(res: any[]): void {res.length = 0; this.response.total = 0; } handleSubmit(searchForm: FormGroup) { this.sho ...

Webpack does not support d3-tip in its current configuration

I'm having some trouble getting d3-tip to work with webpack while using TypeScript. Whenever I try to trigger mouseover events, I get an error saying "Uncaught TypeError: Cannot read property 'target' of null". This issue arises because th ...

Combining multiple Observables and storing them in an array using TypeScript

I am working with two observables in my TypeScript code: The first observable is called ob_oj, and the second one is named ob_oj2. To combine these two observables, I use the following code: Observable.concat(ob_oj, ob_oj2).subscribe(res => { this.de ...

Tips for updating an array in TypeScript with React:

Encountering an issue while updating my state on form submission in TypeScript. I am a newcomer to TS and struggling to resolve the error. enum ServiceTypeEnum { Replace = 'replace product', Fix = 'fix product', } interface IProduc ...

To determine if two constant objects share identical structures in TypeScript, you can compare their properties

There are two theme objects available: const lightMode = { background: "white", text: { primary: "dark", secondary: "darkgrey" }, } as const const darkMode = { background: "black", text: { prim ...