pocketbase / js-sdk

PocketBase JavaScript SDK
https://www.npmjs.com/package/pocketbase
MIT License
2.06k stars 122 forks source link

How to use generics for extending PocketBase collection types? #278

Closed ChrisMGeo closed 8 months ago

ChrisMGeo commented 8 months ago

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]>;
}
ganigeorgiev commented 8 months ago

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.

ChrisMGeo commented 8 months ago

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"]
}
ChrisMGeo commented 8 months ago

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

ganigeorgiev commented 8 months ago

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.

ChrisMGeo commented 8 months ago

Thanks! Sorry I assumed this was an sdk issue.

ganigeorgiev commented 8 months ago

~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.

ganigeorgiev commented 8 months ago

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
}
ChrisMGeo commented 8 months ago

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