garybernhardt/static-path

Request: Allow paths that are not patterns (no parameters)

Closed this issue · 5 comments

I have a number of paths in my application that are not patterns. For example:

import {path} from 'static-path';

export const featureRoot = '/feature';
export const featureSubRoute = path(`${featureRoot}/thing/:thingId`); 

This leads to mixed usage of pathName vs pathName.pattern, pathName(), and pathName.path().

Desired usage:

import {path} from 'static-path';

export const featureRoot = path('/feature');
export const featureSubRoute = featureRoot.path('/thing/:thingId');

Would love to hear your thoughts.

Your desired code works as expected:

export const featureRoot = path('/feature');
export const featureSubRoute = featureRoot.path('/thing/:thingId');

console.log(featureRoot({}))
console.log(featureSubRoute({thingId: "1"}))

// Prints:
// /feature
// /feature/thing/1

For featureRoot({}), featureRoot() is not allowed. Would it be possible to not require the empty object?

A Path is a hybrid type parameterized on the pattern's literal string type:

/* A Path is parameterized only on its pattern. If we ever need the param
 * types, we can use helper types to create them from the pattern type. */
export type Path<Pattern extends string> = {
  (params: Params<Pattern>): string;
  pattern: NormalizePattern<Pattern>;
  parts: Part<Pattern>[];

  path: <Subpattern extends string>(subpattern: Subpattern) => Path<`${Pattern}/${Subpattern}`>;
};

The signature part of that ((params: Params<Pattern>): string) is where it accepts the params object and generates a concrete path string. Two possibilities come to mind:

  1. Overload Path's hybrid type to require an object argument all of the time, except when the params type is exactly {}. I don't think that's possible in TS.
  2. Have the main path constructor function return a different type for paths that have no params. This could probably work, but I think it would add a lot of complexity. Probably too much complexity to justify itself. But I could be wrong!

I may be missing other possibilities. Feel free to give it a try and submit a PR if you want feedback on a potential solution.

Here's what I've come up with by amending Params:

type Params<Pattern extends string> = string extends Pattern 
  ? never 
  : PathParamNames<Pattern> extends never 
    ? void 
    : { [K in PathParamNames<Pattern>]: string; }
}

const courses = path('/course');
courses({});
// Argument of type '{}' is not assignable to parameter of type 'void'.
courses();
// Ok

I additionally tried adding support for optional parameters (despite React Router v6 dropping support):

type RequiredParam<ParamName> = ParamName extends `${infer Name}?` ? never : ParamName;
type OptionalParam<ParamName> = ParamName extends `${infer Name}?` ? Name : never;

type Params<Pattern extends string> = string extends Pattern 
  ? never 
  : PathParamNames<Pattern> extends never 
    ? void 
    : { [K in RequiredParam<PathParamNames<Pattern>>]: string; } & { [K in OptionalParam<PathParamNames<Pattern>>]?: string; };
    
const courses = path('/course');
const course = courses.path(':courseId/:optional?');

course();
// Expected 1 arguments, but got 0.
course({});
// Property 'courseId' is missing in type '{}' but required in type '{ courseId: string; }'.
course({optional: '1'})
// Property 'courseId' is missing in type '{ optional: string; }' but required in type '{ courseId: string; }'.
course({courseId: '1'});
// Ok
course({courseId: '1', optional: '1'})
// Ok

I'm not as well versed in TypeScript as I'd like, so I'm not entirely sure if PathParamNames<Pattern> extends never is valid ("is never?"). I've also seen [T] extends [never] here, not sure the difference.

Closing as I've now done the proper thing by checking out the repo and better understanding the limitations here. If I can come up with something that isn't too complex for what it's worth, I'll open a PR. I may still open a PR for optional params support.