Reputation: 49331
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.
Upvotes: 0
Views: 5182
Reputation: 1565
Just change 'renderer' to 'components':
<ReactMarkdown components={customRenderers}>{blog.content}</ReactMarkdown>
Upvotes: 0
Reputation: 42218
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} = { ...
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.
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;
};
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>
);
};
Upvotes: 4