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

Erase Typescript Service

To remove a PostOffice from the array based on its ID, you can use a checkbox to select the desired element and then utilize its ID for the delete function. Here is an example: let postOffices = [ {postOfficeID: 15, postCode: '3006&ap ...

Determining the data type of an object key in Typescript

Is there a way to limit the indexed access type to only return the type of the key specified? interface User { id: string, name: string, age: number, token: string | null, } interface Updates<Schema> { set: Partial<Record< ...

Can you share the appropriate tsconfig.json configuration for a service worker implementation?

Simply put: TypeScript's lib: ['DOM'] does not incorporate Service Worker types, despite @types/service_worker_api indicating otherwise. I have a functional TypeScript service worker. The only issue is that I need to use // @ts-nocheck at t ...

Tips for specifying a variable as a mandatory key attribute within an array

Is there a way to dynamically determine the type of key attribute in an array? const arr = [ { key: 'a' }, { key: 'b' }, { key: 'c' }, ]; type key = ??? // Possible values for key are 'a', 'b', or &a ...

Exploring the incorporation of interfaces into Vue.js/Typescript for variables. Tips?

I have an interface:   export interface TaskInterface{ name: string description1: string time: string } and a component import { TaskInterface } from '@/types/task.interface' data () { return { tasks: [ { name: 'Create ...

Angular 2 Directive for Ensuring Required Conditions

Is there a way to make form fields required or not based on the value of other fields? The standard RequiredValidator directive doesn't seem to support this, so I've created my own directive: @Directive({ selector: '[myRequired][ngControl ...

Converting a dynamic JSON object into a generic type in TypeScript

I need assistance with converting a JSON object into the equivalent generic type in TypeScript. The JSON object I have contains dynamic keys such as applications and permissions. The keys inside applications, like application_management and user_managemen ...

Investigating TypeScript Bugs in Visual Studio Code

As I navigate through various sources, I notice that there is a plethora of information available on older versions of VSCode (v1.16.1 - the most recent version at the time of writing) or deprecated attributes in the launch.json file. I have experimented ...

Guide on how to specify the return type for useMutation in the 'react-query' library

Here is the code snippet provided: const setFriendCode = (data: Params) => api({ data }) const [mutateSetFriendCode, state] = useMutation<Response, Params>( setFriendCode ) An issue arises with the type of parameters in the code. The compiler ...

Is it feasible to obtain the userId or userInfo from the Firebase authentication API without requiring a login?

Is it feasible to retrieve the user id from Firebase authentication API "email/password method" without logging in? Imagine a function that takes an email as a parameter and returns the firebase userId. getId(email){ //this is just an example return t ...

Adding markers to a map in Angular 2 using ngOnInit after initialization

Embarking on my Angular journey by creating a sample app incorporating GoogleMaps. import { Component, Input, OnInit, Inject } from '@angular/core'; import { HttpClient } from '@angular/common/http'; import { FormControl } from '@ ...

Abort S3 file upload using ASW-SDK

Is there a way to abort an upload without raising an error Upload aborted. when calling upload.abort()? import { PutObjectCommandInput, S3Client } from '@aws-sdk/client-s3'; import { Progress, Upload } from "@aws-sdk/lib-storage"; cons ...

NodeJS on Cloudlinux requires that the node modules for applications be stored in a distinct folder (virtual environment) designated by a symbolic link known as "node_modules"

I recently encountered an issue while trying to deploy my Nodejs/TypeScript web application on my cpanel shared hosting. The error I received stated: * Cloudlinux NodeJS Selector requires the node modules for the application to be stored in a separate f ...

Exploring for JSON keys to find corresponding objects in an array and adding them to the table

I'm currently working on a project where I need to extract specific objects from a JSON based on an array and then display this data in a table. Here's how my situation looks: playerIDs: number[] = [ 1000, 1002, 1004 ] The JSON data that I am t ...

Using the `import.meta` meta-property is restricted to the `es2020`, `esnext`, or `system` options in snowpack+testing-library and will result in an error

I've been encountering issues while setting up Jest and React Testing Library for the snowpack project. The error message I'm receiving is "The 'import.meta' meta-property is only allowed when the '--module' option is 'es ...

Is it possible to refresh the webpage in Angular when the tab is clicked?

Can someone help me find a solution to reload an Angular app's page when the user selects the browser tab? I've been exploring using window.location.reload() for this purpose, but I need guidance on triggering it specifically when the tab is sel ...

An error occured in angular2: Cannot access the 'title' property of undefined

Here is the code snippet for my custom component: export class MoviedetailComponent implements OnInit { movie:any constructor( private getmovie: GetmovieService, private router: Router, private rout: ActivatedRoute ) { } ngOnInit() { this.r ...

Understanding the differences between paths and parameters of routes in express.js

My express application has the following routes: // Get category by id innerRouter.get('/:id', categoriesController.getById) // Get all categories along with their subcategories innerRouter.get('/withSubcategories', categoriesControll ...

Having trouble locating the export in the TypeScript module

Having a situation where there is a file with an exported object: let btypes:{[key:string]:any} = { "key1":val, //... } export {btypes} I even attempted to use export default btypes Upon importing it using: import {btypes} from "../types& ...

transferring libraries via functions in TypeScript

As I work on developing my app, I have decided to implement the dependency-injection pattern. In passing mongoose and config libraries as parameters, I encountered an issue with the config library. Specifically, when hovering over config.get('dbUri&ap ...