next/image の width, height 指定を型レベルで強制する

2022/06/09に公開約4,300字3件のコメント

next/image をラップしたコンポーネントを作る機会があり、せっかくなら width, height の指定を型レベルで強制したいと感じたのでメモ。

next/image のおさらい

next/image は Next.js 10 から正式に使えるようになったコンポーネントで、画像のレスポンシブ対応(サイズやレイアウト)、遅延ロード、webp 変換など様々な画像表示に関する最適化をよしなにやってくれます。

https://nextjs.org/docs/api-reference/next/image

その中でも今回フォーカスを当てるのは、画像の縦横比やレイアウトを指定する際に使う layout プロパティです。

layout, width, height プロパティについて

next/image で画像のサイズに関して指定する layout プロパティは以下のようになっており、これらを指定することによって、viewport が変わった時の挙動などを制御することができます。

https://nextjs.org/docs/api-reference/next/image#layout

以下公式ドキュメントより抜粋 & 簡単に要約。

layout props 挙動
intrinsic デフォルト値。width が viewport 幅よりも小さい場合は viewport 幅に合わせて小さくなりますが、画像の幅が viewport 幅よりも大きい場合は width の値に設定されます。
fixed viewport の幅によらず、設定された width, height の画像を表示します。
responsive viewport 幅に依存して画像幅が変化します。layout='intrinsic'の場合と異なり、画像の幅が viewport 幅よりも大きい場合は viewport 幅に合わせて画像幅が変化します。
fill 親の DOM 要素の height, width に合わせて画像の幅と高さが設定されます。

これらの layout プロパティは layout="fill" を指定したとき以外は、width, height も合わせて指定する必要があります。

しかしデフォルトでは layout="fill" 以外を指定して width, height の指定をしなかったとしても型エラーにはならず、ランタイムエラーになります。

<Image src={"./path/to/image.png"} layout="responsive" />

layout="fill" 以外を指定して width, height の指定を忘れても runtime error になる
layout="fill" 以外を指定して width, height の指定を忘れても runtime error になる

この width, height 指定のエラーを型レベルで気付けるようにしたいというのが今回の趣旨です。

next/image の width, height 指定を型レベルで強制する

いきなりですが、最終的なコードはこんな感じです。

import NextImage, { ImageProps } from "next/image";

// NOTE: 指定したプロパティだけを必須にする utility type 的な型
type RequiredOnly<T, U extends keyof T> = T & Required<Pick<T, U>>;

type WithLayout<T extends ImageProps["layout"]> = ImageProps & {
  layout?: T;
};

// NOTE: layout="fill"以外の時は props の width と height を必須にする
type Props<T extends ImageProps["layout"]> = T extends "fill"
  ? WithLayout<T>
  : RequiredOnly<WithLayout<T>, "width" | "height">;

export const Image = <Layout extends ImageProps["layout"]>(
  props: Props<Layout>
) => {
  const { layout, ...restProps } = props;
  return (
    <div>
      <NextImage {...restProps} layout={layout} />
    </div>
  );
};

順番に何をやってるかみていきましょう。

特定のプロパティだけを必須に

まずは、今回の要件では width, height だけを必須にしたいので特定のプロパティだけを必須にするような utility type を作ります。

type RequiredOnly<T, U extends keyof T> = T & Required<Pick<T, U>>;

ジェネリクスを利用して layout プロパティの型を制限する

次に、ジェネリクスを利用して、layout プロパティの型をより厳しく制限します。

これによって layout プロパティの型は "fill" | "fixed" | "intrinsic" | "responsive" からジェネリクスで1つ指定されたものだけを受け付けるようになります。

type WithLayout<T extends ImageProps["layout"]> = ImageProps & {
  layout?: T;
};

layout プロパティの値によって型を出し分ける

最後に layout プロパティが fill であれば通常の型、そうでなければ、width, height を必須にした型を出し分けてあげれば OK です。

type Props<T extends ImageProps["layout"]> = T extends "fill"
  ? WithLayout<T>
  : RequiredOnly<WithLayout<T>, "width" | "height">;

最終的に、width, height の指定が必須の場合に指定がないと型エラーになるようになりました 🎉

layout="fill" 以外を指定して width, height の指定を忘れると type error になる
layout="fill" 以外を指定して width, height の指定を忘れると type error になる

追記(2022/06/12)

コメントで補足いただいたので、追記です。

Next.js の webpack loader の機能によって静的に import された画像は width, height を自動的に取得してくれるので、layout="fill" 以外を指定した場合でも width, height の指定は不要になっています。

Next.js will automatically determine the width and height of your image based on the imported file. These values are used to prevent Cumulative Layout Shift while your image is loading.

https://nextjs.org/docs/basic-features/image-optimization#local-images

ただ、storybook と next/image を併用する場合はこの機能が利用できないので、layout="fill" 以外を指定した状態で width, height の指定が抜けているとランタイムエラーになります。

そのため本記事で紹介した方法は storybook で next/image を型安全に使用するための tips として活用していただけると幸いです!

参考URL

Discussion

ランタイムエラーを極力無くすための試み、とても素晴らしいと思いました!
その上で一つ補足したい点がございましたので、コメントさせていただきます。

Next.jsに組み込まれているwebpack loaderの機能により、画像をimport(またはrequire)すると、その画像の幅と高さなどを取得してくれます。
そのため、layoutがfillでなくても、src属性にStaticImageDataが指定されている時は、widthとheightを指定する必要はなくなります。

また、next/imageはこの仕様を追加するために、以前はしまさんの定義された型に近い形のものだったのを、今の仕様に変更したと記憶しています!

コメントありがとうございます!

公式に書いてありましたね。。

https://nextjs.org/docs/basic-features/image-optimization#local-images

Next.js will automatically determine the width and height of your image based on the imported file. These values are used to prevent Cumulative Layout Shift while your image is loading.

コメントいただいたので調べていたんですが、storybook だと webpack の loader が next の default と異なるので layout=”fill” 以外の場合に width, height 指定がないとランタイムエラーになるみたいで、その問題を解決したいというのが本記事の趣旨でした!

ただ静的に指定された画像なら Next.js が自動で width, height を判断してくれるのは理解しておらず storybook で問題なく表示させたいという前提が抜けてたので、追記させていただきます 🙇

勉強になりました、ありがとうございます!!

なるほど!!
こちらこそ勉強になりました!!

記事への追記も丁寧にありがとうございます。
これからもしまさんの発信楽しみにしております〜

ログインするとコメントできます