Quick disclaimer: What you're about to read covers only about 20% of the type errors I encountered. Initially, I hadn't planned to write this blog post - I was just trying to get my markdown renderer working. But after the fifth cup of coffee and the twentieth type error, I thought, "Someone else needs to benefit from this suffering." So here we are.
Good news: At the end of this post, I'm sharing four production-ready template files that you can use as a starting point for your own implementation. These templates have saved my team countless hours, and hopefully, they'll do the same for you.
Why This Implementation Matters: The AI Landscape Context
Let me provide some context about why this implementation became crucial for our team. We're building made it look straightforward:
<ReactMarkdown
components={{
code: ({ node, ...props }) => <SomeComponent {...props} />,
}}
/>
But TypeScript had other plans. The moment I tried this "simple" approach, I was greeted with the cryptic error: "Property 'inline' does not exist on type '{}'". And thus began my journey through the type system maze.
The Evolution of Solutions
Let me walk you through the progression of attempts that led to our final solution. Each one taught me something valuable about TypeScript's type system.
First, I started with what seemed logical - using React's built-in type definitions:
type CodeProps = React.ComponentProps<"code">;
const CodeComponent = ({ inline, className, children }: CodeProps) => {
// Implementation that was doomed from the start
};
This approach failed because React's native types don't include ReactMarkdown-specific properties. A rookie mistake, but one that taught me to look deeper into library-specific type definitions.
Next, I tried using ReactMarkdownOptions:
import { ReactMarkdownOptions } from "react-markdown/lib/react-markdown";
const CodeComponent: ReactMarkdownOptions["components"]["code"] = ({
inline,
className,
children,
}) => {
// Getting closer, but still not quite there
};
Finally, after much research and experimentation, I found the solution that worked:
import { Components } from "react-markdown/lib/ast-to-react";
const CodeComponent: Components["code"] = ({
className,
children,
...props
}) => {
const match = /language-(\w+)/.exec(className || "");
const lang = match && match[1];
return <CodeBlock lang={lang || "text"} codeChildren={String(children)} />;
};
The Display Name and Circular Reference Challenges
Just when I thought I had everything under control, ESLint started complaining about missing display names. Here's how we solved it while maintaining type safety:
export const components: Partial<Components> = {
code: Object.assign(CodeComponent, { displayName: "CodeComponent" }),
// ... other components
};
The circular reference crisis we encountered led to this type-safe solution:
const extractTextContent = (node: React.ReactNode): string => {
if (typeof node === "string") return node;
if (typeof node === "number") return String(node);
if (Array.isArray(node)) return node.map(extractTextContent).join("");
if (React.isValidElement(node)) {
return extractTextContent(node.props.children);
}
return "";
};
Best Practices We Learned the Hard Way
Through this journey, we discovered several crucial best practices:
Always use the Components type from
react-markdown/lib/ast-to-reactfor custom components - it provides the most complete type definitions for all ReactMarkdown component properties.Be extremely careful with import paths - using the wrong import path can lead to incomplete type definitions. Always import from
react-markdown/lib/ast-to-reactwhen working with custom components.When using
React.memowithReactMarkdowncomponents, apply the type definition before memoization. This ensures proper type inference and prevents hard-to-debug type errors later.Never use
@types/react-markdowndirectly - let the types come from thereact-markdownpackage itself. The DefinitelyTyped types can sometimes be outdated or incomplete.Always provide explicit display names for your components, even though it might seem redundant. This becomes crucial for debugging and React DevTools usage.
When dealing with children props, always implement proper type-safe content extraction. This prevents runtime errors from circular references.
Production-Ready Templates and Implementation
As promised, I'm sharing our battle-tested templates: : The main component that handles markdown rendering
: A reusable code block component with syntax highlighting
|
SOCIAL SHARE CARD GENERATOR