Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Extended path parameter type-safety #122

Open
janbuchar opened this issue Sep 18, 2023 · 7 comments
Open

Extended path parameter type-safety #122

janbuchar opened this issue Sep 18, 2023 · 7 comments

Comments

@janbuchar
Copy link

Is there a way to make url generation for path parameters such as /blog/:postId accept only numbers (provided that postId should be a number) via typescript? Writing this kind of logic manually is super repetitive and I believe it should be possible to simplify it with code generation.

@oedotme
Copy link
Owner

oedotme commented Sep 26, 2023

Hey @janbuchar, could you please give an example with some context of the router functions/hooks you're using?

From what I understand, the params type would always be a Record<string, string> as it's derived from the URL string. I don't know if it's useful in this case to use Number(params.param) to cast it on a type/runtime level at the places require the usage of the param as a number.

Related to that, as I'm considering supporting type-safe search params - which similar to path param, derived from the URL and location state - would be a flexible type.

@janbuchar
Copy link
Author

janbuchar commented Sep 26, 2023

A concise example would be a route such as /articles/:articleId. I would love to be able to say for instance

type PathParams {
  articleId: number
}

somewhere in the page file and then call useParams("/articles/:articleId") and get PathParams from that.

Does this make sense?

Of course, doing something similar for search params would also be helpful. But if that was possible, I think it would be even more justified to want the same for path parameters - there is no fundamental difference between these two.

@oedotme
Copy link
Owner

oedotme commented Sep 27, 2023

@janbuchar that's exactly what I thought about as well. But what I'm concerned about is on the runtime level all params would be strings, so declaring a param as number would be only on the type level.

As an example:

export type Params = { id: number }

const params = useParams('/invoice/:id')
//    ^? { id: number }

typeof params.id === 'string'

I'm concerned about the potential mismatch here, please let me know what you think.

Maybe the search params can be handled differently if somehow values are decoded/encoded to preserve the types. I haven't tried something similar before.

@janbuchar
Copy link
Author

I'm glad I'm not the only one who would consider this useful. I believe there'd have to be some runtime encoder/decoder for types like number or Date, and again, that applies to both path params and search params. Of course, plugging this into useParams would probably only be possible with code generation.

@oedotme
Copy link
Owner

oedotme commented Oct 13, 2023

@janbuchar the problem even if we have a route-based type for params and search params, the built-in useParams and useSearchParams wouldn't follow the types on the runtime level.

I believe the codegen would only make it easier, it can start as something similar to next-typesafe-url for types and runtime validation. Let me know what you think.

@janbuchar
Copy link
Author

I see. Yes, having runtime conversion of path or search parameters rules out using anything directly imported from react-router, tanstack/router and the like. Or, using those just forfeits the type safety added by the runtime conversion.

Having something like the package you linked is very close to what I had in mind 🙂

@FrameMuse
Copy link

FrameMuse commented Nov 9, 2023

I love that I'm not the only one that was thinking about this. Personally, I use this snippet to address this problem, for me it's enough, but maybe for you it might become inspiration.

useParam.ts

import { useParams } from "react-router-dom"

function useParam<N extends boolean = false>(paramKey: string, numeric?: N): N extends true ? number : string {
  const params = useParams()
  const paramValue = params[paramKey]

  if (paramValue == null) {
    throw new Error(`Coundn't find param ${paramKey}. It should be declared in a route path.`)
  }
  if (numeric && isNaN(Number(paramValue))) {
    throw new Error(`Param ${paramKey} is not number.`, { cause: { paramValue } })
  }

  return paramValue as N extends true ? number : string
}

export default useParam

When the error is thrown I catch it with ErrorBoundary.

I use it to validate only numbers, but I guess you could spread this to Date also.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants