edgedb / edgedb-js

The official TypeScript/JS client library and query builder for EdgeDB
https://edgedb.com
Apache License 2.0
510 stars 65 forks source link

Question: "Cannot find namespace 'e' when using Typescript types from edgeql-js #270

Closed johanbrook closed 2 years ago

johanbrook commented 2 years ago

Hi! Sorry for a "question issue", but I just can't get past this myself.

But when I try to do use a generated TS type from the package, I run into this TS error:

error TS2503: Cannot find namespace 'e'

Code:

import e from './edgeql-js'; // auto-generated

// can't use my own types from the schema:
//   error TS2503: Cannot find namespace 'e'
let c: e.Contact = {};

// not even built-types work:
//   error TS2503: Cannot find namespace 'e'
let s: e.str = e.str('hey');

I'm following the docs in "Objects and paths", which says:

All object types in your schema are reflected into the query builder, properly namespaced by module. ... For convenience, the contents of the default module are also available at the top-level of e.

Are the docs talking about the Typescript type here? Or is it a reference to the EdgeQL schema type?

Thoughts

Versions

Typescript: 4.5.5
EdgeDB (npm): 0.19.8

Many thanks!

1st1 commented 2 years ago

@colinhacks @jaclarke

Sikarii commented 2 years ago

You are importing the library right, however the default export, in this case e is a big object (not a typescript type!). You can see this behaviour when you hover over your e import, it shows up as a constant object (at least in VS Code).

If you wanted to get the typeof User in e, you would do typeof e.User, however this probably is not what you're looking for. Importing import { $ContactλShape } from "./edgeql-js/modules/default" might work out for you.

I'm not 100% sure on what the preferred way of getting the schema type is, hope this helps though.

johanbrook commented 2 years ago

Thanks for the reply!

Yea, I've toyed around with various kinds of import type and typeof e.User but to no avail 😄 I've read the source of the generated code, and $ContactλShape seemed very ... non-public and scary to use in app code.

My angle is from GraphQL, where you'd generate TS types for two things:

  1. The schema itself (objects, enums, scalars, ...)
  2. Queries and mutations — their result types and input variables.

Is it so that 1) and 2) are inferred by TS with thanks to edgeql-js? Meaning, no "concrete" types are generated, but everything is meant to be inferred when taking care of query results.

Example:

export const findContacts = async () ⇒
   e.select(e.Contact, () ⇒ {
      name: true,
      // email: true,
   }).run(client);

This is all dynamic as I manipulate the fields in the query. Which is pretty cool. If I'd have a "concrete" type for the function, it'd go out of sync easily with need for generating new TS types from the query:

// ! This would go out of sync
import type { ContactQuery } from './some-generated-schema.ts`;

export const findContacts = async (): Array<ContactQuery> ⇒ { ... }

I've solved my particular issue with this:

// db.ts

export type FindContacts = Awaited<ReturnType<typeof findContacts>>;

export const findContacts = async () ⇒ { … };
// someComponent.ts
import type { FindContacts } from './db';

const Contacts = (contacts: FindContacts) ⇒ {
   return (
      <ul>
        {/* This is now type safe */}
         {contacts.map(c ⇒ <li>{c.name}</lI>)}
       </ul>
   );
}

With that said, it'd be nice to get a hold of the concrete types from the schema somehow.

jaclarke commented 2 years ago

@sikarii's right, the default export "e" of the querybuilder is an object not a namespace. The reason is that type names in EdgeDB can contain pretty much any unicode character, so we can't export them as normal identifiers and instead everything has to be keys on an object.

Since queries in EdgeQL can be composed of any arbitrary expressions (where you can choose which properties of an object get returned, traverse links, define computed props, etc..), currently there are no real 'concrete' types in the querybuilder, and everything has to be inferred for each specific query. There is a helper type: $infer, which you can use to get the return type that you would get if you called .run() on any expression:

import e, {$infer} from './edgeql-js';

const query = e.select(e.Contact, () => ({
  name: true,
  email: true
}));

export type Contacts = $infer<typeof query>;

export async function findContacts() { // Return type would be Promise<Contacts>
  return await query.run(client);
}

What kind of types would you find useful as concrete types? Something like this?:

interface AddressBook {
  contacts?: Contact[];
}

interface Contact {
  name?: string;
  email?: string;
}
johanbrook commented 2 years ago

@jaclarke Gotcha, thanks. That confirms what I ultimately arrived at too.

$infer looks really cool, thanks! No need for ReturnType

As mentioned, I came from GraphQL where the server schema and client queries are generated into TS types. The latter might be hard to pull off with edgeql-js then, but the former would be handy.

Imagine this scenario:

// email.ts
// ! This is where I'd make use of a "concrete" type Contact !
import type { Contact } from './schema.ts'; // auto-generated

export const sendEmailTo(contacts: Array<Pick<Contact, 'email' | 'someOtherProp'>>) ⇒ {
  // ...
};
server.ts
import { sendEmailTo } from './email';
import { findContacts } from './db';

const main = async () ⇒ {
   const contacts = await findContacts();
   sendEmailTo(contacts);
};

Seems like this is what the $XXXXXλShape types are, in modules/default.ts no?

colinhacks commented 2 years ago

The $XXXXXλShape types are very different - that is a complex data structure representing that EdgeDB type. You can introspect what this type looks like by playing around with autocompletion:

e.Movie.__element__.__pointers__;
// $MovieλShape
Screen Shot 2022-03-01 at 2 59 49 PM

We don't currently generate "plain" types like what you are describing, though I think we should. It'll probably look something like this:

import e, {types} from "./dbschema/edgeql-js";
type Movie = types["default"]["Movie"];
// {title: string; actors: Person, ...}

Should be possible to do @jaclarke thoughts? Internally it'll look something like this:

interface default$$User {
  name: string;
  filmography: default$$Movie[];
}
interface default$$Movie {
  name: string;
  actors: default$$User[];
}
interface default$$ {
  User: default$$User;
  Movie: default$$Movie;
}
export interface types {
  default: default$$;
}

type asdf = types['default']['User']['filmography']
jaclarke commented 2 years ago

PR for this here: https://github.com/edgedb/edgedb-js/pull/281