0

I could not find anything, neither on GitHub nor index.d.ts of the package react.markdown. It looks like a very simple example, yet the entire Google does not contain any example. I wrote a custom renderer for a Markdown component, but I could not figure out the type for the renderer:

import ReactMarkdown, { Renderers, Renderer, NodeType } from "react-markdown";

const PostContent: React.FC<PostContent> = ({ blog }) => {
  const customRenderers: Renderers = {
    paragraph(paragraph) {
      const { node } = paragraph;
      if (node.children[0].type === "image") {
        const image = node.children[0];
        return (
          <div style={{ width: "100%", maxWidth: "60rem" }}>
            <Image
              src={`/images/posts/${blog.slug}/${image.src}`}
              alt={image.alt}
              width={600}
              height={300}
            />
          </div>
        );
      }
    },
  };

  return (
    <article className="">
      <ReactMarkdown renderers={customRenderers}>{blog.content}</ReactMarkdown>
    </article>
  );
};

What is the type of paragraph? I checked; it is Renderer:

   type Renderer<T> = (props: T) => ElementType<T>

I could not figure out what to pass as T. I tried, HtmlParagraphElement or any.

Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
Yilmaz
  • 35,338
  • 10
  • 157
  • 202
  • The types define the props of `paragraph` as `any`. I'm not sure what they actually are. You will get rid of all TS errors if you type `customRenderers` as `{[nodeType: string]: ElementType}` which is the type of the `renderers` prop. I've got to play with this to figure out what the `any` actually is! – Linda Paiste Mar 14 '21 at 20:29
  • 1
    I thought we could make use of the types from the underlying markdown package but react-markdown renames some of the props! https://github.com/remarkjs/react-markdown/blob/236182ecf30bd89c1e5a7652acaf8d0bf81e6170/src/ast-to-react.js#L70 – Linda Paiste Mar 14 '21 at 20:59
  • @LindaPaiste my brain is about to burn. What is this? it is not an npm package, and react-markdown installed module has no src. – Yilmaz Mar 14 '21 at 21:08
  • These are the types for react-markdown: https://github.com/remarkjs/react-markdown/blob/236182ecf30bd89c1e5a7652acaf8d0bf81e6170/index.d.ts There's not much there. There is nothing there that tells you what props a `paragraph` element gets called with. But you can see that the types for a node rely on an imported type `mdast.Content` Those `mdast` types include all of the possible node and the node properties. https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/types/mdast/index.d.ts So we can make use of mdast types to get strict typing for `paragraph.node` (cont.) – Linda Paiste Mar 14 '21 at 21:21
  • Using some TS magic here https://codesandbox.io/s/react-markdown-typescript-yi7ij?file=/src/App.tsx I got the `customRenderers` map to recognize all of the properties that are present on the `node` (`props.node`) based on the key name (`paragraph`, `image`, etc.). This should work and you get typed access to everything in its "raw" form. – Linda Paiste Mar 14 '21 at 21:24
  • However there are other properties that are added to `props` by the react-markdown package which do not have typescript definitions anywhere. What's happening in the source code file that I linked is their mapping of properties on the `node` to custom top-level properties. For example an `image` gets called with `props` that include `props.node` which has properties `{ type, title, url, alt, position }` -- the stuff that we know based on the mdast types -- but it also gets `props.src`, `props.alt`, and `props.title`, which are the untyped, unndocumented top-level props. – Linda Paiste Mar 14 '21 at 21:28
  • @LindaPaiste you could write those as an answer instead of comment. – Yilmaz Mar 14 '21 at 21:34

2 Answers2

4

Renderer Type

The react-markdown package is very loosely typed. It declares the type of renderers as an object map

{[nodeType: string]: ElementType}

where the keys can be any string (not just valid node types) and the values have the type ElementType imported from the React typings. ElementType means that your renderer can be a built-in element tag name like "p" or "div" or a function component or class component that takes any props.

You could just type your object as

const customRenderers: {[nodeType: string]: ElementType} = { ...

Typing the Props

ElementType is not at all useful for getting type safety inside the render function. The type says the the props can be anything. It would be nice if we could know what props our render function is actually going to be called with.

Our paragraph gets called with props node and children. A code element gets called with props language, value, node and children. The custom props like the language and value are unfortunately not documented in Typescript anywhere. You can see them being set in the getNodeProps function of the react-markdown source code. There are different props for each node type.

Typing the Node

The props node and children are where we can actually get useful Typescript information.

The react-markdown types show that the type for a node is the Content type imported from the underlying markdown parser package mdast. This Content type is the union of all individual markdown node types. These individual types all have a distinct type property which a string literal that matches the key that we want to set on our renderers object!

So finally we know that the type for valid keys is Content["type"]. We also know that the node prop for a specific K key will be Extract<Content, { type: K }> which gives us the member of the union that matches this type property.

The children prop on the props object is just a typical React children prop, but not all node types have children. We we can know whether or not our props include children by looking at the type for the node and seeing whether or not it has a children property.

type NodeToProps<T> = {
  node: T;
  children: T extends { children: any } ? ReactNode : never;
};

(this matches the received props because the children prop is always set, but will be undefined if children are not supported)

So now we can define a strict type for your customRenderers -- or any custom renderer map:

type CustomRenderers = {
  [K in Content["type"]]?: (
    props: NodeToProps<Extract<Content, { type: K }>>
  ) => ReactElement;
};

Conditional Overriding

Your code will intercept all paragraph nodes, but will only return any content when the condition node.children[0].type === "image" is met. That means all other paragraphs get removed! You need to make sure that you always return something.

const PostContent: React.FC<PostContent> = ({ blog }) => {
  const customRenderers: CustomRenderers = {
    // node has type mdast.Paragraph, children is React.ReactNode
    paragraph: ({ node, children }) => {
      if (node.children[0].type === "image") {
        const image = node.children[0]; // type mdast.Image
        return (
          <div
            style={{
              width: "100%",
              maxWidth: "60rem"
            }}
          >
            <img
              src={`/images/posts/${blog.slug}/${image.src}`}
              alt={image.alt}
              width={600}
              height={300}
            />
          </div>
        );
      } 
      // return a standard paragraph in all other cases
      else return <p>{children}</p>;
    },
  };

  return (
    <article className="">
      <ReactMarkdown renderers={customRenderers}>{blog.content}</ReactMarkdown>
    </article>
  );
};

Code Sandbox Link

Linda Paiste
  • 38,446
  • 6
  • 64
  • 102
  • Hi Linda. I am having issue here ` const { language, value } = code;`. it says "Property 'language' does not exist on type 'NodeToProps'" – Yilmaz Mar 17 '21 at 22:58
  • Right so you are trying to access the custom properties that are hard to type so I haven't included them. With this type definition you would need to do `const {node} = code; const {lang, value} = node;` I think `value` should be there? I know it's `lang` and not `language` on the `node`. – Linda Paiste Mar 17 '21 at 23:02
  • This will be last issue. Thank you for your help so far. renderers does not accept type CustomRenderers. gives me this error. – Yilmaz Mar 18 '21 at 11:21
  • "Type 'CustomRenderers' is not assignable to type '{ [nodeType: string]: ElementType; }'. Property 'paragraph' is incompatible with index signature. Type '((props: NodeToProps) => ReactElement ReactElement | null) | (new (props: any) => Component)>) | undefined' is not assignable to type 'ElementType'. Type 'undefined' is not assignable to type 'ElementType'.ts(2322) index.d.ts(45, 14): The expected type comes from property 'renderers' which is declared here on type '(IntrinsicAttributes & .... – Yilmaz Mar 18 '21 at 11:22
  • Your renderer always has to return something in order to be a valid react component. If you want to display nothing then return null. But you cannot use the custom render only in certain situations. If you don’t return anything then then node won’t show up at all. – Linda Paiste Mar 18 '21 at 13:58
  • renderer returns component in both cases. I currently solved the issue with this, renderers={customRenderers as any}. thank you for your help – Yilmaz Mar 18 '21 at 14:35
  • The error message *Type 'undefined' is not assignable to type 'ElementType'* implies that there is a missing return somewhere. Like an `if` that's missing an `else`. If you post it a typescript playground or codesandbox I can look at it. – Linda Paiste Mar 18 '21 at 19:49
0

Just change 'renderer' to 'components':

<ReactMarkdown components={customRenderers}>{blog.content}</ReactMarkdown>
Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
Sehrish Waheed
  • 1,230
  • 14
  • 17