Home > database >  Map object into tuple type with generics
Map object into tuple type with generics

Time:10-14

I have an object:

config: {
  someKey: someString
}

And I want to obtain a tuple type according to that config. So:

function createRouter<
  T extends Record<string, string>
>(config: T) {
  type Router = {
    // How to get hold of T[K] below, such that, e.g.:
    // ['someKey', typeof someString]
    navigate: [keyof T, T[K]];
  };
}

Playground

Expanding on this a little. I need this Router type because I'm providing it to a function that returns an object of functions whose the parameters are based on the original config. In that function, the return object looks like this:

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

Where T in this case is the Router type I'm passing in. An example usage is like this:

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

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

In the example above, navigate, and user and id are all inferred. The tuple [keyof T, T[K]] is spread into the navigate function, so keyof T is the first param, and T[K} is the second, (and I am also transforming T[K] to extract the :id parameter, but that doesn't matter for this question).

If config has more than one key, then, the type should still be inferred correctly, so:

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

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

Note that I'm not asking how to transform 'user/:userId', as I already have the type that converts that into an object type. I just need the config in the form of a tuple.

CodePudding user response:

So, you need to declare the config for the router as const, to let the generics trickle down. Then you need to split path by / and only keep parts that include :${param}. From here out, you construct an object with these 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

Playground: link
Source: https://lihautan.com/extract-parameters-type-from-string-literal-types-with-typescript/

CodePudding user response:

I'm not sure exactly what you want anymore, but I'll try help a bit.

The navigate function needs to be generic to be able to infer the correct params for the route, so spreading a tuple type is not possible.

The following should at least be a good start:

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

// The `Infer` for the parameter is to make TS infer the exact values of the properties, instead of just `string`.
// The builtin `Readonly` works, too. It just has to be a mapped type.
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" });

As an aside, the following is my "parameter extractor" (without the optional params part):

/**
 * 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
>;
  • Related