pocketbase/js-sdk

How to use generics for extending PocketBase collection types?

ChrisMGeo opened this issue · 8 comments

Hello, I'm making an app using pocketbase and typescript, and was trying to type out the collections as per docs, however using a generic instead. This seems to add the right collections but only has the type safety if I use collection<"name">("name") and not collection("name"). If I comment the default one with any string, it works but obviously gets rid of compatibility with any generic string.

export interface TypedPocketBase<T extends { [x: string]: RecordModel }> extends PocketBase {
  collection(idOrName: string): RecordService
  collection<K extends keyof T>(idOrName: K): RecordService<T[K]>;
}

I don't think this is a SDK issue, or at least I don't understand the described problem.

I've copied locally the example from the Specify TypeScript definitions section and it works fine for me.

If you are still not able to identify the culprit, please provide a more complete code sample, the version of TS and its configuration that you use.

I don't think this is a SDK issue, or at least I don't understand the described problem.

I've copied locally the example from the Specify TypeScript definitions section and it works fine for me.

If you are still not able to identify the culprit, please provide a more complete code sample, the version of TS and its configuration that you use.

Sorry for the late reply, but here is my complete code sample (I'm using Next.js 14):

// types/pocketbase.ts
import PocketBase, { type RecordService, type RecordModel } from "pocketbase";

export interface TypedPocketBase<T extends { [x: string]: RecordModel }> extends PocketBase {
  collection(idOrName: string): RecordService
  collection<K extends keyof T>(idOrName: K): RecordService<T[K]>;
}

export type Room = {
  title: string;
  description: string;
} & RecordModel;
// consts/pocketbase.ts
import type { Room, TypedPocketBase } from '@/types/pocketbase';
import PocketBase from 'pocketbase';

const db = new PocketBase("http://127.0.0.1:8090") as TypedPocketBase<{
  rooms: Room;
}>;
if (process.env.NODE_ENV === "development") db.autoCancellation(false);
export { db };

Here is what goes wrong when I use it:
Without using <"rooms">:
image
With using <"rooms">:
image

TypeScript version in package.json:

 "typescript": "^5"

TSConfig (Next.js):

{
  "compilerOptions": {
    "lib": ["dom", "dom.iterable", "esnext"],
    "allowJs": true,
    "skipLibCheck": true,
    "strict": true,
    "noEmit": true,
    "esModuleInterop": true,
    "module": "esnext",
    "moduleResolution": "bundler",
    "resolveJsonModule": true,
    "isolatedModules": true,
    "jsx": "preserve",
    "incremental": true,
    "plugins": [
      {
        "name": "next"
      }
    ],
    "paths": {
      "@/*": ["./*"]
    }
  },
  "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
  "exclude": ["node_modules"]
}

I'm assuming this has something to do with how the definition in the type uses <K extends keyof T>?

I think I understand what you are trying to do but I'm not sure what is the best way to approach it as I'm not so versatile with TypeScript.

The problem with just using K extends keyof T is that in your case that will be the key of the map (aka. "room"), but you want both key and value. Following this one possible workaround could be to try something like:

collection<K extends keyof T, M extends T[K]>(idOrName: K): RecordService<M>;

Testing locally the above complile but again I'm not sure if this is the best way to do it and you may have a better chance resolving your issue asking in the TypeScript support channel for help.

Thanks! Sorry I assumed this was an sdk issue.

Also I forgot to mention that the above work for me only if I remove the plain fallback "collection(idOrName: string): RecordService" (I'm not sure exactly why?):

export interface TypedPocketBase<T extends { [x: string]: RecordModel }> extends PocketBase {
  collection<K extends keyof T, M extends T[K]>(idOrName: K): RecordService<M>
  collection(idOrName: string): RecordService
}

Update: It seems that the order of the declarations matter, so if you put it as last one you still can have a default fallback.

Oh sorry, on second read I see that you've used T[K] so you don't need the extra M.
In other words, I think your problem in this case is that the TS will see the plain-non generic default version and will pick that since it is the first non-concrete type so in your code sample it should be enough to just place it as last option (if you need it):

export interface TypedPocketBase<T extends { [x: string]: RecordModel }> extends PocketBase {
  collection<K extends keyof T>(idOrName: K): RecordService<T[K]>
  collection(idOrName: string): RecordService
}

Oooh didn't know that keeping it last had that effect. Thanks again for the help!