解決 MDX 元件的 TypeScript 型別問題

MDX 的標誌

環境與套件

"next-mdx-remote": "^4.1.0",
"@stitches/react": "^1.2.8",
"next": 12.3.0,
"react": 18.2.0

也適用於另一款 MDX 套件:

"mdx-bundler": 9.0.1

MDX 的好處是把 Markdown 加上各種想要的元件,達到容易維護又能高度客製化。

之前已經在試用 Remix 時體驗過,而最近要用到 Next.js + TypeScript 的時候,遇到許多型別錯誤的問題。翻閱 Google 和 Stackoverflow 許久都沒什麼幫助,只好向社群求助,記錄在這篇文章。


接下來的內容


以 MDX 的元件取代 Markdown 產生的標籤

MDX 可以把 Markdown 的語法以 React 元件取代(MDX 說明文件),next-mdx-remotemdx-bundler 也有。

const mdxComponents = {
  h2: Heading2,
  h3: Heading3,
  p: ArticleP,
  ul: ArticleUL,
  ol: ArticleOL,
  a: ArticleLink,
  img: ArticleImage,
  blockquote: ArticleBlockquote,
  hr: () => <Divider position="article" />
};

// ...

<MDXRemote {...source} components={mdxComponents} />

還有一種方法是直接加入元件,並放在 .mdx 內文即可使用,更有彈性。

const mdxComponents = {
  LyricSection
};

只有 children 的 TypeScript 設定

從 TypeScript 語法提示得知,傳入的 props 型別只要依照 HTML 的標準即可。標籤的 children 大多是選填,因此要加上問號:

interface ChildrenProps {
  children?: React.ReactNode;
}

const Paragraph = styled('p', {
  '&:not(blockquote p)': {
    fontSize: '$20',
    lineHeight: '$32'
  }
});

export default function ArticleP({ children }: ChildrenProps) {
  return (<Paragraph>{children}</Paragraph>);
};

圖片與超連結標籤的 TypeScript 設定

圖片與超連結就不是單純的只有 children,因此遇到了 TypeScript 型別錯誤。

圖片 <img>

Next.js 有非常方便的 next/image,但是,文章內的圖片寬高都不是固定的。而每個圖片都另外做 React 元件,都失去了 Markdown 的便利性。因此就把 Markdown 產生的 <img> 取代為 <img loading="lazy">

圖片語法可以傳入 3 個屬性:srcalttitle

![alt 取代文字](src "title")

它們都是選填的,因此設定型別時得:

interface ImageProps {
  src?: string;
  alt?: string;
  title?: string;
}

使用 Next.js 時,通常是先用 next/link 包住 <a>,並以 passHref 傳遞 href

然而,hrefnext/link 是必填,但原生 HTML 語法是選填,使用以下的型別設定

interface ArticleLinkProps {
  children?: React.ReactNode;
  href: string;
}

就會出現型別錯誤:

Type '{ a: ({ children, href }: ArticleLinkProps) => JSX.Element; }' is not assignable to type 'MDXComponents'.
  Type '{ a: ({ children, href }: ArticleLinkProps) => JSX.Element; }' is not assignable to type '{ a?: keyof IntrinsicElements | Component<DetailedHTMLProps<AnchorHTMLAttributes<HTMLAnchorElement>, HTMLAnchorElement>> | undefined; ... 173 more ...; view?: keyof IntrinsicElements | ... 1 more ... | undefined; }'

# ...還有一大串

在前端社群問到的方法:

// ArticleLink.tsx
import type { ComponentProps } from 'react';
import Link from 'next/link';
import { StyledLink } from './styled';

export default function ArticleLink({ children, href }: ComponentProps<'a'>) {
  if (!href) {
    throw new Error('should pass href as props')
  }

  return (
    <Link href={href} passHref>
      <StyledLink>{children}</StyledLink>
    </Link>
  );
};

Props 的型別設定為 ComponentProps<'a'>,並在沒有 href 的情況丟出錯誤即可。

點選 Codesandbox 試試看。


💬 討論

新增討論 👆連至 Github 的 Discussions 參與