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

feat: declare document TypeScript types in createClient() #238

Merged
merged 10 commits into from
Jun 17, 2022

Conversation

angeloashmore
Copy link
Member

@angeloashmore angeloashmore commented Apr 24, 2022

Types of changes

  • Chore (a non-breaking change which is related to package maintenance)
  • Bug fix (a non-breaking change which fixes an issue)
  • New feature (a non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)

Description

This PR adds the ability to declare document types queryable by the client.

Client methods that accept an explicit document type, such as getByType() and getSingle(), can automatically return the appropriate document type. Furthermore, these methods are typed to only accept document type IDs that have been provided to the client.

import * as prismic from "@prismicio/client";
import * as prismicT from "@prismicio/types";

type PageDocument = prismicT.PrismicDocument<
  { foo: prismicT.KeyTextField },
  "page"
>;

type SettingsDocument = prismicT.PrismicDocument<
  { bar: prismicT.BooleanField },
  "settings"
>;

type AllDocumentTypes = PageDocument | SettingsDocument;

const client = prismic.createClient<AllDocumentTypes>("qwerty");
//    ^ Contains references to document types

const home = await client.getByUID("page", "home");
//    ^ Typed as PageDocument

home.data.foo;
//        ^ Typed as KeyTextField

const settings = await client.getSingle("settings");
//    ^ Typed as SettingsDocument

const foo = await client.getByType("foo");
// Type error - argument of type "foo" is not assignable to parameter of type "page" | "settings"

Methods that do not explicitly accept a document type return a union of all possible document types. Documents can be type narrowed at runtime by checking their type property.

const client = prismic.createClient<AllDocumentTypes>("qwerty");

const doc = await client.getFirst();
//    ^ Typed as PageDocument | SettingsDocument

Maintaining backwards compatibility

Query methods still accept a DocumentType type parameter to explicitly declare the returned document type. This is useful if types were not provided to createClient() or the document type passed to a query method is not a constant (i.e. typed as string).

Backwards compatibility is maintained compared to the previous TypeScript API.

const client = prismic.createClient("qwerty");

const homeWithoutType = await client.getByUID("page", "home");
//    ^ Typed as prismicT.PrismicDocument (i.e. a generic document)

const homeWithType = await client.getByUID<PageDocument>("page", "home");
//    ^ Typed as PageDocument

If types are provided to createClient(), explitly provided document types to a query method must be assignable to the client-level types.

type FooDocument = prismicT.PrismicDocument<
  { baz: prismicT.BooleanField },
  "foo"
>;

const client = prismic.createClient<PageDocument | SettingsDocument>("qwerty");

const foo = await client.getSingle<FooDocument>("settings");
// Type error - FooDocument is not assignable to PageDocument | SettingsDocument

Client type augmentation

This PR also adds the ability for createClient() type augmentation through a new CreateClient interface. This lets third-party packages, such as those generating types, to automatically link a literal repository name to a set of document types.

// prismic-client.d.ts
import * as prismic from "@prismicio/client";
import * as prismicT from "@prismicio/types";

type PageDocument = prismicT.PrismicDocument<
  { foo: prismicT.KeyTextField },
  "page"
>;

type SettingsDocument = prismicT.PrismicDocument<
  { bar: prismicT.Boolean },
  "settings"
>;

type AllDocumentTypes = PageDocument | SettingsDocument;

declare module "@prismicio/client" {
  interface CreateClient {
    (
      repositoryNameOrEndpoint: "qwerty",
      options?: prismic.ClientConfig | undefined
    ): prismic.Client<AllDocumentTypes>;
  }
}

With this module augmentation in place, createClient("qwerty") will automatically use AllDocumentTypes.

import * as prismic from "@prismicio/client";

const client = prismic.createClient("qwerty");
//    ^ Contains references to document types

const home = await client.getByUID("page", "home");
//    ^ Typed as PageDocument

home.data.foo;
//        ^ typed as KeyTextField

Checklist:

  • My change requires an update to the official documentation.
  • All TSDoc comments are up-to-date and new ones have been added where necessary.
  • All new and existing tests are passing.

🦑

@codecov-commenter
Copy link

codecov-commenter commented Apr 24, 2022

Codecov Report

Merging #238 (0759f1e) into master (f93af08) will not change coverage.
The diff coverage is 100.00%.

@@            Coverage Diff            @@
##            master      #238   +/-   ##
=========================================
  Coverage   100.00%   100.00%           
=========================================
  Files           23        23           
  Lines          363       363           
  Branches        67        67           
=========================================
  Hits           363       363           
Impacted Files Coverage Δ
src/index.ts 100.00% <ø> (ø)
src/client.ts 100.00% <100.00%> (ø)

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update f93af08...0759f1e. Read the comment docs.

@github-actions
Copy link

github-actions bot commented Apr 24, 2022

size-limit report 📦

Path Size
dist/index.js 4.09 KB (-0.1% 🔽)
dist/index.cjs 6.98 KB (-0.02% 🔽)

@angeloashmore angeloashmore requested a review from lihbr April 24, 2022 21:05
@angeloashmore angeloashmore marked this pull request as draft April 24, 2022 22:12
@lihbr lihbr added the enhancement New feature or request label Apr 25, 2022
Copy link
Member

@lihbr lihbr left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice job, looks good to me!

Maybe it's worth adding an example for the CreateClient extends? Up to you!

src/client.ts Outdated
Comment on lines 243 to 252
export interface CreateClient {
<
DocumentTypeMap extends Record<string, prismicT.PrismicDocument> = Record<
string,
prismicT.PrismicDocument
>,
>(
...args: ConstructorParameters<typeof Client>
): Client<DocumentTypeMap>;
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there any value to allow extension of the whole createClient() function type?
As-in, can we make it so that you only need to extend the document type map?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is needed to pair a repository name literal to its document types. This allows this new system to support apps with multiple repositories, each with their own set of document types.

For example:

declare module "@prismicio/client" {
  interface CreateClient {
    (
      repositoryNameOrEndpoint: "foo",
      options?: prismic.ClientConfig | undefined
    ): prismic.Client<FooDocumentTypes>;
    (
      repositoryNameOrEndpoint: "bar",
      options?: prismic.ClientConfig | undefined
    ): prismic.Client<BarDocumentTypes>;
  }
}

While this is a rare situation, it ensure we are not locked into only supporting a single global config.

Since this code would typically be generated rather than hand-written, the extra boilerplate is okay, I think.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK, I was thinking it'll be something like that, great!

@lihbr
Copy link
Member

lihbr commented Apr 25, 2022

From @chamois-d-or (Alexandre): Any consideration about graph queries?

@angeloashmore
Copy link
Member Author

angeloashmore commented Apr 26, 2022

Thanks for the review! 🙂 (And the nice refactor to use Exclude over a map!)

Maybe it's worth adding an example for the CreateClient extends? Up to you!

Augmenting CreateClient is more of an internal API for now. I think we can omit a CreateClient example until we want or see a need for external tools to extend it.

I'll update the "With TypeScript" example to use this new API. Manual usage (i.e. without touching CreateClient) is now a bit simpler than before, and is more type-safe.

Maybe we should add another example for JSDoc.

Copy link
Member

@lihbr lihbr left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perfect 👌

@angeloashmore
Copy link
Member Author

Oops, I missed this question:

From @chamois-d-or (Alexandre): Any consideration about graph queries?

Automatically typing GraphQuery or FetchLinks is out of scope for this PR. This PR still allows developers to manually extend the return type like this:

import * as prismic from "./index";
import * as prismicT from "@prismicio/types";

type PageDocument = prismicT.PrismicDocument<
	{
		foo: prismicT.KeyTextField;
		parent: prismicT.LinkField;
	},
	"page"
>;

type AllDocumentTypes = PageDocument;

const client = prismic.createClient<AllDocumentTypes>("qwerty");

const graphQuery = `
{
	page {
		parent {
			...on page {
				title
			}
		}
	}
}
`.trim();

// Manually extend PageDocument to include the GraphQuery fields.
const home = (await client.getByUID("page", "home", {
	graphQuery,
})) as PageDocument & {
	data: {
		parent: {
			data: {
				title: prismicT.TitleField;
			};
		};
	};
};

The "page" parameter will still be type checked against PageDocument.

In the future, we could support an automatic GraphQuery/FetchLinks => TypeScript conversion that would simplify the above approach, but this is not planned currently:

const home = (await client.getByUID("page", "home", {
	graphQuery,
})) as PageDocument & PageParentGraphQuery;

@angeloashmore angeloashmore marked this pull request as ready for review June 17, 2022 19:48
@angeloashmore angeloashmore changed the title feat: declare document types with createClient() feat: declare document TypeScript types in createClient() Jun 17, 2022
@angeloashmore angeloashmore merged commit 92d8f84 into master Jun 17, 2022
@angeloashmore angeloashmore deleted the aa/document-types branch June 17, 2022 19:49
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants