Structured Text with Typescript

Hello everyone! Completely new to DatoCMS after working with Strapi for a while, and I’m having major issues getting the typescript codegen things to work.

I’ve followed this guide but I’m not having any success.

For one, the graphql.config.yml file has a spelling error in the custom scalars:
ā€œJsonField: unkownā€ should be unknown, right?

But when I fix that I get a type error on the StructuredText component:

"Type '{ __typename?: "StartpageModelContentField" | undefined; value: unknown; } | null | undefined' is not assignable to type 'Document | Node | StructuredText<Record, Record> | null | undefined'."
... 
"Type 'unknown' is not assignable to type 'Document'"

In other similar issues on here, a suggested solution is to make the JsonField scalar have the type " StructuredText<Record, Record>", but StructuredText doesn’t get imported in to the generated.ts file, and if I import it manually I’m getting the error ā€œGeneric type ā€˜Record’ requires 2 type argument(s).ts(2314)ā€

Any help would be much appreciated, it’s definitely possible I’m doing something else completely wrong.

Hello @koskarbengtsson and welcome to the community!

Can you attempt to change it to

JsonField: "StructuredText<Record, Record>

In your graphql.config.yml and let me know if it works after re-generating?

Thank you for the swift response! I get errors in the generated.ts file itself now, but it did fix the error in the components. What I also had to do was restructure the .yml file to this:

...

plugins:
      - typescript
      - typescript-operations
      - typed-document-node
    config:
      strictScalars: true
      scalars:
        BooleanType: boolean
        CustomData: Record<string, string>
        Date: string
        DateTime: string
        FloatType: number
        IntType: number
        ItemId: string
        JsonField: "StructuredText<Record, Record>"
        MetaTagAttributes: Record<string, string>
        UploadId: string
      namingConvention:
        enumValues: "./src/lib/pascalCaseWithUnderscores.js"

Putting the custom scalars indented inside the typescript-operations plugin made all the scalars in the generated file ā€œanyā€ for some reason.

Any ideas on how to fix the type errors in generated.ts? It won’t compile and can’t be excluded in my tsconfig.

I’ve sort of kinda made it work by changing my .yml file to this:

generates:
  src/graphql/generated.ts:
    plugins:
      - typescript
      - add:
          content: "import { type Record as StructuredTextGraphQlResponseRecord, type StructuredText as StructuredTextGraphQlResponse } from 'datocms-structured-text-utils';"
      - typescript-operations
      - typed-document-node
    config:
      strictScalars: true
      scalars:
        BooleanType: boolean
        CustomData: Record<string, string>
        Date: string
        DateTime: string
        FloatType: number
        IntType: number
        ItemId: string
        JsonField: "StructuredTextGraphQlResponse<StructuredTextGraphQlResponseRecord,StructuredTextGraphQlResponseRecord>"
        MetaTagAttributes: Record<string, string>
        UploadId: string
      namingConvention:
        enumValues: "./src/lib/pascalCaseWithUnderscores.js"

and then adding the full ā€œpage.content.valueā€ prop to the StructuredText data field.
But I imagine that would break cases where you need to use blocks or links instead of the value.

I’m facing the same issue with this one. Any chance of an update with a proper answer for this @m.finamor? @koskarbengtsson said so himself, that using blocks or links would break and I need to use links in my case. I’m getting errors in my generated.ts file:
Generic type 'Record' requires 2 type argument(s).

Hi @john.hult, welcome! Could you please provide a bit more detail?

I tried following the guide and using the example repo along with a structured text field with a linked record, and it seems to work without any errors.

I didn’t modify the graphql.config.yml, but I did edit graphql/home.graphql in that example to fetch the structured context field and its links, like:

query Home {
  allArticles {
    id
    title
    _createdAt
    _publishedAt
    image {
      ...responsiveImage
    }
    content {
      value
      links {
        id
        title
        content {
          value
        }
      }
    }
  }
}

(The content is what I added over the example)

Then I as able to run graphql-codegen without any errors.

What specifically are you trying to do that is failing with an error? Any error messages, screenshots, sample code, etc. would be very helpful :slight_smile:

I ended up doing a custom validation for the value,links,block field. The problem was that we had something like this:

someStructuredText {
      value
      links {
        __typename
        ... on PageRecord {
          pageTitle
          url
          id
        }
      }
    }

and then a component that wraps the StructuredContent as such

<Content
    data={parseStructuredText(someStructuredText)}
    linkProps={{ bold: true, underline: true }}
/>

and lastly, we have the Content component like this

interface R1 extends StructuredTextGraphQlResponseRecord {}
interface R2 extends StructuredTextGraphQlResponseRecord {}

interface ContentProps {
  data:
    | StructuredTextGraphQlResponse<R1, R2>
    | StructuredTextDocument
    | Node
    | null
    | undefined;
...

Here, TS would complain when sending the data, as it would not match, since value in this case would be unknown.

I ended up creating the parseStructuredText as you see in the code above, to verify that value exists else I’d just get:

So I did the following:

function isStructuredText(
  text: unknown
): text is StructuredTextGraphQlResponse {
  const t = text as StructuredTextGraphQlResponse;
  return (
    t &&
    typeof t === "object" &&
    "value" in t &&
    validate(t.value as Document).valid
  );
}

/**
 * if t contains value and is validated as a Document ()
 * @param t your structured text
 * @returns Structured Text
 */
export function parseStructuredText(
  t: unknown
): StructuredTextGraphQlResponse | undefined {
  return isStructuredText(t)
    ? (t as unknown as StructuredTextGraphQlResponse)
    : undefined;
}

which solves it for now. But it just feels like structured text should have a proper type.

@john.hult, thank you so much for sharing the details! Please give us a few days to look more deeply into it, and we’ll report back here ASAP.

Glad the workaround works for now though.

1 Like

I was able to solve this issue without introducing additional login in the application code.
It involves some extra configuration, but hopefully it will help ppl looking for a solution:

What we need to do is merge DatoCMS schema with custom one, that will override StructuredText value. That can be done by utilising schema loader:

graphql.config.ts

  schema: {
    'https://graphql.datocms.com': {
      headers: {
        Authorization: process.env.DATOCMS_CLIENT_TOKEN!,
        'X-Exclude-Invalid': 'true'
      },
      loader: './schema-loader.js'
    }
  },

schema-loader.js

const {loadSchema} = require('@graphql-tools/load')
const {mergeTypeDefs} = require('@graphql-tools/merge')

module.exports = async (schemaString, config) => {
  const schema = await loadSchema(schemaString, config)

  return mergeTypeDefs(
    [
      `
        scalar StructuredTextDocument
        type BlogpostModelContentField {
          value: StructuredTextDocument!
        }
      `,
      schema
    ],
    {
      // if conflict is detected, use custom definition
      onFieldTypeConflict: (customField, _schemaField) => {
        return customField
      }
    }
  )
}

We introduce a custom scalar value and use it to override JsonField. In the config, we need to include needed import that will be added to generated types:

graphql.config.ts

generates: {
    'src/types/graphql.ts': {
      plugins: [
        {
          add: {
            content:
              "import { type Document as StructuredTextDocument } from 'datocms-structured-text-utils';"
          }
        },
        'typescript',
        'typescript-operations'
      ],
      config: {
        strictScalars: true,
        scalars: {
          StructuredTextDocument: 'StructuredTextDocument',
          // and others...
        }
      }
    }
  }

All this results in proper types being generated. No need for type guards or type overriding in the app code.

3 Likes