TypeScript: Restrict function parameter value to values of array within dynamic variable

I have a simple react hook that returns a function. I would like this function to only accept values from an array that gets passed into the hook.

Heres the hook:

type Options<P> = {
  pathname: string;
  rootPath: string;
  paths: P;
};

export const useLayoutPaths = <P extends string[]>(options: Options<P>) => {
  const { pathname, rootPath, paths } = options;

  if (paths.length === 0) {
    throw new Error("paths must be a non-empty array.");
  }

  const currentPath = pathname.substring(pathname.lastIndexOf("/") + 1);
  const value = paths.includes(currentPath) ? currentPath : paths[0];

  const getPath = (name: typeof paths[number]): string =>
    `${rootPath}/${String(name)}`;

  return { value, getPath };
};

I would like the getPath function to only allow values that exist within the "paths" variable.

Heres my current usage:

const { value, getPath } = useLayoutPaths({
  pathname,
  rootPath: `/department/${orgId}`,
  paths: ["trends", "comments"],
});

console.log(getPath("trends")) <-- Only allow "trends" or "comments"

>Solution :

You want the compiler to keep track of the literal type of the strings passed in as the paths property. Unfortunately this did not happen with a generic constraint like P extends string[]. One way to increase the likelihood that the compiler will treat "foo" as being of type "foo" instead of type string is to constrain a generic type parameter to string. So P extends string will work better. We can just have P be the type of the elements of paths instead of paths itself, like this:

export const useLayoutPaths = <P extends string>(options: Options<P[]>) => {
   // impl
};

This will work as desired, but the compiler doesn’t like letting you look up a string in an array of P. See this question and answer for more information. The easiest way to deal with that is to widen paths from P[] to readonly string[] before using includes():

export const useLayoutPaths = <P extends string>(options: Options<P[]>) => {
  const { pathname, rootPath, paths } = options;

  if (paths.length === 0) {
    throw new Error("paths must be a non-empty array.");
  }

  const currentPath = pathname.substring(pathname.lastIndexOf("/") + 1);
  const value = (paths as readonly string[]).includes(currentPath) ? currentPath : paths[0];

  const getPath = (name: typeof paths[number]): string =>
    `${rootPath}/${String(name)}`;

  return { value, getPath };
};

And now things work as you want:

console.log(getPath("trends")) // okay
getPath("oops"); // error!

Playground link to code

Leave a Reply