prismicio / prismic-react

React components and hooks to fetch and present Prismic content
https://prismic.io/docs/technologies/homepage-reactjs
Apache License 2.0
154 stars 40 forks source link

Typescript + SliceZone - error #144

Closed kb1995 closed 2 years ago

kb1995 commented 2 years ago

Hey,

I encounter another Typescript error, which might be my fault, but I can't figure out why is it happening.

My pages/index.tsx

import { SliceZone } from "@prismicio/react";
import { components } from "slices/essential_slices";

....

<SliceZone slices={doc.data.slices} components={components} />

I get the following error on the components props

Type '{ content: ({ slice }: { slice: any; }) => JSX.Element; cta: ({ slice }: { slice: any; }) => JSX.Element; faq: ({ slice }: { slice: any; }) => JSX.Element; form: ({ slice }: { slice: any; }) => JSX.Element; hero: ({ slice }: Props) => JSX.Element; testimonial: ({ slice }: Props) => JSX.Element; }' is not assignable to type 'SliceZoneComponents<SliceLike<string>, unknown>'.
  Property 'hero' is incompatible with index signature.
    Type '({ slice }: Props) => JSX.Element' is not assignable to type 'SliceComponentType<SliceLike<string>, unknown>'.
      Type '({ slice }: Props) => JSX.Element' is not assignable to type 'FunctionComponent<SliceComponentProps<SliceLike<string>, unknown>>'.
        Types of parameters '__0' and 'props' are incompatible.
Type 'PropsWithChildren<SliceComponentProps<SliceLike<string>, unknown>>' is not assignable to type 'Props'.
            Types of property 'slice' are incompatible.
              Type 'SliceLike<string>' is missing the following properties from type 'Slice': primary, slice_label, variation, version, itemsts(2322)
[index.d.ts(476, 5): ]()The expected type comes from property 'components' which is declared here on type 'IntrinsicAttributes & SliceZoneProps<SliceLike<string>, unknown>'

This is what my Hero slice looks like

import { SharedSlice, KeyTextField, RichTextField } from "@prismicio/types";

interface Props {
  slice: Slice;
}

interface Slice extends SharedSlice {
  primary: {
    eyebrow: KeyTextField;
    title: RichTextField;
    subtitle: RichTextField;
    image: any;
  };
}

const Hero = ({ slice }: Props) => (
...
angeloashmore commented 2 years ago

Hi @kb1995,

This happens because the type used your Props interface is more specific than what <SliceZone> knows. It looks like doc.data.slices is not typed to be a SliceZone that contains a hero Slice.

As a result, <SliceZone> only knows that doc.data.slices contains an array of objects containing at least a slice_zone property. Your Slice component, however, is asking for more data, such as your primary fields.

In this case, your Slice component's props can only be typed with a "generic" Slice using @prismicio/react's SliceComponentProps, like this:

import { SliceComponentProps } from "@prismicio/react";

const Hero = ({ slice }: SliceComponentProps) => {
  return <div>Your component</div>;
};

This is not ideal since you no longer have a fully typed slice prop.

How to fully type your Slice components

To fully type your Slice components, this is what I recommend you do:

  1. Add a type for your Hero Slice using the SharedSlice type (or Slice if you aren't using Prismic's Slice Machine tool). It lets you define its primary and item fields without using extend.

    Repeat this for each of your Slice component files.

    import { SharedSliceVariation, KeyTextField, RichTextField, ImageField } from "@prismicio/types";
    import { SliceComponentProps } from "@prismicio/react";
    
    export type HeroSlice = SharedSlice<
     "hero",
     SharedSliceVariation<
       "default_slice",
       {
         eyebrow: KeyTextField,
         title: RichTextField,
         subtitle: RichTextField,
         image: ImageField,
       },
       never
     >
    >;
    
    type HeroProps = SliceComponentProps<HeroSlice>;
    
    const Hero = (props: HeroProps) => {
     return <div>Test</div>;
    };
    
    export default Hero

    Note that HeroSlice is exported from the file.

  2. Where you query for your document, type the document. In this example, I've included a Slice for Hero, Text, and CallToAction, but replace these with your actual Slices.

    import { PrismicDocument, SliceZone } from "@prismicio/types";
    
    import Hero, { HeroSlice } from "../slices/Hero";
    import Text, { TextSlice } from "../slices/Text";
    import CallToAction, { CallToActionSlice } from "../slices/CallToAction";
    
    type PageDocument = PrismicDocument<
     "page",
     {
       body: SliceZone<HeroSlice | TextSlice | CallToActionSlice>,
     }
    >;
    
    // When you query for your document
    const doc = client.getByUID<PageDocument>("page", "home");
    
    // When you use SliceZone, it should be fully typed now
    <SliceZone
     slices={doc.data.slices}
     components={{
       hero: Hero,
       text: Text,
       call_to_action: CallToAction,
     }}
    />

    I'm assuming you're using something like Next.js where you use @prismicio/client directly. If you are using a different framework like Gatsby, let me know and I can adjust the response.

Hope that helps! I'm going to close this issue, but please let me know if this doesn't work and I'll try to help. 🙂

Also, we plan to automate this TypeScript type set up process. If you're curious, you can see some experiments here: https://github.com/prismicio/prismic-ts-codegen

prismic-ts-codegen is not something we recommend using today, but feel free to try it out to get a better understanding of how you can build your own document and Slice types.

kb1995 commented 2 years ago

@angeloashmore Thanks for the answer!

It looks like it's quite difficult to maintain types manually for slices at the moment and it would make the codebase quite messy. Would you recommend to wait for ts-codegen to be in beta before adding Typescript support to our slices?