16
Help us understand the problem. What are the problem?

posted at

updated at

Organization

TypeScript x React x Storybook のプロジェクトを CSF3.0 対応させようとして型問題でテンパったら読む記事

はじめに

先日 Storybook 6.4 がリリースされ、CSF 3.0 がフィーチャーフラグなしで使えるようになりました。
意気揚々と移行を試みたものの、story object に適用できる型がかなり増えており、どれを使うのが正解か理解するのに時間がかかりました。
同じように悩んでいる方の助けになればと思い、備忘録として調べた内容を残しておきます。

TL;DR

Storybook 6.4 であることが前提ですが、とりあえず CSF 3.0 なら ComponentStoryObj, CSF 2.0 なら ComponentStoryFn 使っておけば間違いないでしょう。

Meta と Story --Storybook 6.2 以前の story object の型定義

Storybook 6.2 における story の書き方を振り返ってみましょう。
※ フォーマットは CSF 2.0 に準拠しているので、storiesOf API 使ってる方の参考にはならないかもしれません。ごめんなさい。

// SomeComponent.stories.tsx

import { Meta, Story } from "@Storybook/react";
import React from "react";

import { SomeComponent, SomeComponentProps } from "..";

const meta: Meta = {
  title: "SomeComponent",
  component: SomeComponent,
};
export default meta;

export const MyStory: Story<SomeComponentProps> = (args) => (
  <SomeComponent {...args} />
);
MyStory.args = {
  // 省略
};

ここで登場する型は Meta と Story の 2 つのみです。型情報は以下から確認できます。
https://github.com/storybookjs/Storybook/blob/v6.2.9/app/react/src/client/preview/types-6-0.ts

ComponentMeta と ComponentStory --Storybook 6.3 で登場した新しい型

Storybook 6.3 から新たに ComponentMeta, ComponentStory という型が登場しました。
これは既存の Meta, Story と何が違うのでしょうか?ライブラリが提供している型ファイルを見てみましょう。
https://github.com/storybookjs/Storybook/blob/v6.3.0/app/react/src/client/preview/types-6-3.ts

// types-6.3.d.ts

/**
 * For the common case where a component's stories are simple components that receives args as props:
 *
 * ```tsx
 * export default { ... } as ComponentMeta<typeof Button>;
 * ```
 */
export declare type ComponentMeta<
  T extends keyof JSX.IntrinsicElements | JSXElementConstructor<any>
> = Meta<ComponentProps<T>>;
/**
 * For the common case where a story is a simple component that receives args as props:
 *
 * ```tsx
 * const Template: ComponentStory<typeof Button> = (args) => <Button {...args} />
 * ```
 */
export declare type ComponentStory<
  T extends keyof JSX.IntrinsicElements | JSXElementConstructor<any>
> = Story<ComponentProps<T>>;

ComponentMeta は Meta のエイリアス、ComponentStory は Story のエイリアスであることがわかります。
また ComponentMeta, ComponentStory を使う場合は型引数が必須となり、この時渡せるのは story のベースとなるコンポーネントの型のみです。
これらの新しい型の登場によって、 Storybook のためだけにコンポーネントの props を export する必要がなくなりました。

ちょうどこの issue への回答で ComponentMeta, ComponentStory という型を追加した意図が説明されています。

ただまあ、実はこんな風に書けばコンポーネントの props を export せずに済んではいたのですが……
Story<React.ComponentProps<typeof SomeComponent>>

だとしてもやっぱりこっちの方がスッキリしてて嬉しいですよね! :blush:
ComponentStory<typeof SomeComponent>

Storybook 6.4 での変更

Storybook 6.4 から、CSF 3.0 をデフォルトでサポートするようになりました。これによって型ファイルにも大幅な変更が加えられています。
これから型ファイルの中身を見ていきますが、長くなるので先にまとめておくと、storybook 6.4 で登場する型は全部でこんな感じです。
えらい多いですね。

Meta これまでの Meta と同じ。
ComponentMeta Meta 型のエイリアス。 Storybook 6.3 の ComponentMeta と同じ。
StoryFn CSF 2.0 向けの型。詳細後述。
ComponentStoryFn StoryFn のエイリアス。型引数に story のベースとなるコンポーネントの型を入れて使う。
StoryObj CSF 3.0 向けの型。詳細後述。
ComponentStoryObj StoryObj のエイリアス。型引数に story のベースとなるコンポーネントの型を入れて使う。
Story 今のところは StoryFn のエイリアス。 Storybook 7.0 では StoryObj のエイリアスに変更される。
ComponentStory Story のエイリアス。型引数に story のベースとなるコンポーネントの型を入れて使う。

types-6.3.d.ts にアップデートが入り、 ComponentXX 系の型が増えています。まずはそちらから見ていきましょう。

ComponentXX 系の型の扱い方

結論から言うと ComponentXX 系の型はすべて XX という型のエイリアスです。
ComponentMeta, ComponentStory の時と同様、型引数に story のベースとなるコンポーネントの型を渡して使います。
https://github.com/storybookjs/Storybook/blob/v6.4.0/app/react/src/client/preview/types-6-3.ts

どちらを使っても構いませんが、個人的には書き心地がスッキリする ComponentXX 型の方がいいなと感じています。

続いて新しく追加された StoryObj, StoryFn 等の型について確認していきます。
で結局どの型使えばいいの?についてですが、これは story の構成と CSF のバージョンによって変わってきます。
順に見ていきましょう。

Storybook 6.4 x CSF 2.0 の場合

結論: ComponentMeta と ComponentStoryFn がおすすめ

最初に貼ったコンポーネントの story のコード例の一部を再掲します。
Storybook のバージョンを上げても、CSF のバージョン移行をしていなければ、基本的に story のコードは変更なしでも動きます。
しかしながら、参照している型の内容には変化が生じています。

// SomeComponent.stories.tsx

// Story 型は StoryFn のエイリアスとなった
const MyStory: Story<SomeComponentProps> = (args) => (
  <SomeComponent {...args} />
);

// ので、こう書いてもよい
const MyStory: StoryFn<SomeComponentProps> = (args) => (
  <SomeComponent {...args} />
);

// こう書くこともできる
const MyStory: ComponentStory<typeof SomeComponent> = (args) => (
  <SomeComponent {...args} />
);

// ということは、こう書いても良いことになる
const MyStory: ComponentStoryFn<typeof SomeComponent> = (args) => (
  <SomeComponent {...args} />
);

型情報
https://github.com/storybookjs/Storybook/blob/v6.4.0/app/react/src/client/preview/types-6-0.ts
https://github.com/storybookjs/Storybook/blob/v6.4.0/app/react/src/client/preview/types-6-3.ts

上記のどの書き方でも今は問題なく動くのですが、型ファイル中に NOTE that in Storybook 7.0, this type will be renamed to StoryFn and replaced by the current StoryObj type. とあるように、 Storybook 7.0 にアップデートされた時点で Story 型は StoryFn ではなく StoryObj のエイリアスとなります。
少しわかりにくいですが、 Storybook の migration guide にもそのように書いてあります。

よって、 特に理由がなければ StoryFn 型を採用する方がよいでしょう。
前述の通り、コードがスッキリするので ComponentStoryFn の方が個人的には好みです。

Storybook 6.4 x CSF 3.0 の場合

結論: ComponentMeta と ComponentStoryObj がおすすめ

CSF 3.0 から story の記述方法がガラっと変わります。当初のサンプルコードを CSF 3.0 流に書き直すとこんな感じになります。

// SomeComponent.stories.tsx

import SomeComponent from "..";

export default { component: SomeComponent };

export const Sample = {
  args: {
    // 省略
  },
};

CSF 2.0 では関数だった story がオブジェクトになりました。ここまで来ればもう StoryFn と StoryObj の使い分けに迷うことはなさそうです。
ではここに型をあてていきます。型がなくてもエラーにならない場合もありますが、型推論が有効になった方が何かと嬉しいですしね。

// SomeComponent.stories.tsx

import SomeComponent from "..";

const meta: ComponentMeta<typeof SomeComponent> = { component: SomeComponent };
export default Meta;

// 公式ドキュメントでは type assertion を使って以下のようにするのが推奨されていたのですが、
// export default { component: SomeComponent } as ComponentMeta<typeof SomeComponent>;
// 個人的に type assertion は型エラーを握り潰してしまう可能性があるのであまり使いたくなく、上記のように書き直しています。

export const Sample: ComponentStoryObj<typeof SomeComponent> = {
  args: {
    // 省略
  },
};

Storybook 7.0 以降は Story 型が StoryObj のエイリアスとなるので、ほんの少し短く書けるようになります。
3 文字短縮されるだけなのでまあ、どちらでもいいと言えばいいですね。

// SomeComponent.stories.tsx for Storybook 7.0

export const Sample: ComponentStory<typeof SomeComponent> = {
  args: {
    // 省略
  },
};

まとめ

ドキュメントにはっきりとした記載がなかったので、 Storybook のバージョンを上げ下げして型ファイルを確認したり CSF のドキュメントを睨みつけたりして結構大変だった気がしてたんですが、こうしてまとめてみると案外シンプルな話でした。

  • ComponentXX 型は XX 型のエイリアスであり、型引数にコンポーネントの型をそのまま渡せるところが便利
  • Storybook 6.4 から Story 型は StoryFn のエイリアスとなった
  • でも Storybook 7.0 以降の Story 型は StoryObj のエイリアスに変わるので、バージョンアップのタイミングで直しておきましょう
  • ↑ とはいえ、それ直すくらいなら早めに CSF 3.0 対応済ませちゃった方がいいと思います

今年はこんな感じで大丈夫か?

大丈夫だ、問題ない


ありがとうございました!!

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Sign upLogin
16
Help us understand the problem. What are the problem?