solidjs / solid-router

A universal router for Solid inspired by Ember and React Router
MIT License
1.13k stars 143 forks source link

Typing: Using basic interface as generic type argument for `useParams` #301

Closed stazz closed 7 months ago

stazz commented 11 months ago

Describe the bug

The following TypeScript code results in compilation error:

import { useParams } from "@solidjs/router";

export default function MyComponent() {
  const params = useParams<MyParams>();
  return <div>{params.something}</div>;
}

export interface MyParams {
  something: string;
}

The error is this:

Type 'MyParams' does not satisfy the constraint 'Params'.
  Index signature for type 'string' is missing in type 'MyParams'. ts(2344)

And it is because the useParams looks like this:

// routing.d.ts
export declare const useParams: <T extends Params>() => T;
// types.d.ts
export type Params = Record<string, string>;

If the Params type definition would be changed from Record<string, string> to object, then the error would go away.

Your Example Website or App

https://example.com/not-relevant

Steps to Reproduce the Bug or Issue

  1. Write the code in the description
  2. Observe TS error

I am using Typescript version 5.2.2 with the following config

{
  "compilerOptions": {
    "module": "ESNext",
    "moduleResolution": "node",
    "target": "ESNext",
    "lib": [
      "DOM",
      "ESNext"
    ],
    "esModuleInterop": true,
    "sourceMap": false,
    "strict": true,
    "exactOptionalPropertyTypes": true,
    "declaration": false,
    "noErrorTruncation": true,
    "incremental": true,
    "isolatedModules": true,
    "allowSyntheticDefaultImports": true,
    "noEmit": true,
    "jsx": "preserve",
    "jsxImportSource": "solid-js",
    "rootDir": "./src",
    "tsBuildInfoFile": "./build/tsconfig.tsbuildinfo",
    "resolveJsonModule": true
  },
  "include": [
    "src/**/*",
    "vite.config.ts"
  ],
  "ts-node": {
    "esm": true,
    "experimentalSpecifierResolution": "node"
  },
}

Expected behavior

As a user, I expect that using simple interface would not cause any errors.

There is a workaround for this problem:

import { useParams } from "@solidjs/router";

export default function MyComponent() {
  const params = useParams<MyParamsForRouter>();
  return <div>{params.something}</div>;
}

export interface MyParams {
  something: string;
}

type MyParamsForRouter = MyParams & {
  [P in string]: never;
}

Screenshots or Videos

No response

Platform

Irrelevant, as the bug is on compilation level.

Additional context

I want my routed component not to be too much aware of exact routing structure, therefore I want to expose the parameters it expects as explicit interface. This interface is then used as generic argument to useParams by the component, as well as in type assertions higher up in hierarchy where <Router> and <Route> components are used:

import { lazy } from "solid-js";
import { Routes, Route } from "@solidjs/router";
import type { MatchFilters, MatchFilter } from "@solidjs/router/dist/types";
// Notice import _type_ to make lazy-loading actually possible
import type myComponentTypes from "./my-component";

const MyComponent = lazy(() => import("./my-component"));

export default function App() {
  return (
    <Routes>
      <Route
        path={ROUTE_PATH}
        matchFilters={MATCH_FILTERS}
        component={MyComponent}
      />
    </Routes>
  );
}

const ROUTE_PATH = "/:something";

const MATCH_FILTERS = {
  something: /.+/
} satisfies {
  [P in keyof (MatchFilters<typeof ROUTE_PATH> &
    myComponentTypes.MyParams)]-?: MatchFilter;
}

This way, I get type-safe (in terms of myComponentTypes.MyParams) route path string as well as match filters.

femincan commented 8 months ago

Thanks for your feedback @stazz.

The problem is not caused by solid-router, it is caused by TypeScript. In TypeScript you cannot use a specific interface with a more general one. You can check this issue, and this answer for more information.

In your case, you cannot use the MyParams interface as the generic type because it is more specific than the Record type. Also, your suggestion won't work because object means any object type like null, array, function, and so on. So it doesn't guarantee type safety.

My recommendation for you is to use type instead of interface. This will solve the error from TypeScript.

ryansolid commented 7 months ago

Reviewing this I see that this is just a TS-ism I conferred with my experts and they said no action.