T-CREATOR

Motion(旧 Framer Motion)× TypeScript:Variant 型と Props 推論を強化する設定レシピ

Motion(旧 Framer Motion)× TypeScript:Variant 型と Props 推論を強化する設定レシピ

Motion(旧 Framer Motion)を使ったアニメーション実装は、React 開発において非常に人気の高い選択肢です。しかし、TypeScript と組み合わせた際に Variant の型定義や Props の推論が弱くなり、型安全性が損なわれる場面に遭遇したことはありませんか。

本記事では、Motion と TypeScript を組み合わせる際の Variant 型定義の強化方法と、Props 推論を最大限活用するための実践的な設定レシピをご紹介します。これらのテクニックを習得することで、より安全で保守性の高いアニメーションコードを書けるようになるでしょう。

背景

Motion(旧 Framer Motion)とは

Motion は、React アプリケーションにおいて宣言的なアニメーションを実装するためのライブラリです。以前は Framer Motion という名称でしたが、2024 年にリブランディングされ、現在は Motion として提供されています。

このライブラリの最大の特徴は、variants という仕組みを使って複雑なアニメーションを宣言的に記述できる点にあります。しかし、この便利な variants を TypeScript で扱う際には、型定義が緩くなりがちで、開発体験が損なわれるケースが多く見られます。

以下の図は、Motion における基本的なアニメーションフローを示しています。

mermaidflowchart LR
  developer["開発者"] -->|定義| variants["Variants オブジェクト"]
  variants -->|props 経由| motion["motion コンポーネント"]
  motion -->|アニメーション実行| dom["DOM 要素"]
  state["状態管理"] -->|variant 名| motion

TypeScript との組み合わせで生じる課題

Motion を TypeScript プロジェクトで使用する際、以下のような課題に直面します。

型推論の弱さ: variants オブジェクトを定義しても、そのキー名が自動で推論されないため、variant 名を文字列リテラルで指定する際にタイポが発生しやすくなります。

Props の型安全性: initialanimateexit などの props に渡す値が、定義した variants のキー名と一致しているかをコンパイル時にチェックできません。

拡張性の問題: カスタムの motion コンポーネントを作成する際に、variants の型情報を適切に伝播させるのが困難です。

課題

Variant 型定義の問題点

Motion の公式ドキュメントで紹介されている基本的な variants の記述方法は、以下のようなものです。

typescriptconst variants = {
  hidden: { opacity: 0 },
  visible: { opacity: 1 },
};

このコードは動作しますが、TypeScript の恩恵をほとんど受けられません。具体的には以下の課題があります。

文字列リテラルの自動補完が効かない: animate="visible" と書く際に、IDE が "visible" という選択肢を提示してくれません。

タイポの検出ができない: animate="visble" のようなタイポがあっても、コンパイルエラーにならず、実行時まで気づけないでしょう。

リファクタリングの困難さ: variant 名を変更する際、すべての使用箇所を手動で修正する必要があり、変更漏れのリスクが高まります。

Props 推論の限界

Motion コンポーネントの props は、以下のように多様な値を受け入れます。

typescript<motion.div
  initial='hidden'
  animate='visible'
  exit='hidden'
  variants={variants}
/>

しかし、initialanimateexit に渡せる文字列が、variants で定義したキー名に限定されていることを TypeScript が理解できません。このため、存在しない variant 名を指定してもエラーにならず、バグの温床となります。

下図は、型安全性が弱い場合に発生する問題フローを示しています。

mermaidflowchart TD
  code["コード記述"] -->|タイポあり| compile["コンパイル成功"]
  compile -->|実行| runtime["ランタイムエラー"]
  runtime -->|デバッグ| debug["原因特定に時間"]
  debug -->|修正| code

  code2["型安全なコード"] -->|型チェック| error["コンパイルエラー"]
  error -->|即座に修正| code2

解決策

Variant 型を厳密に定義する

Motion と TypeScript の型安全性を強化するには、variants オブジェクトの型を明示的に定義し、その型情報を活用する必要があります。

ステップ 1:型定義のためのユーティリティ型を作成

まず、variants の型情報を抽出するためのユーティリティ型を定義します。

typescriptimport { Variants } from 'motion/react';

// Variants のキー名を Union 型として抽出
type VariantKeys<T extends Variants> = keyof T & string;

この VariantKeys 型は、variants オブジェクトからキー名だけを Union 型として抽出します。& string を付けることで、symbolnumber 型のキーを除外し、文字列リテラルの Union 型だけを得られます。

ステップ 2:型安全な variants を定義

次に、実際の variants オブジェクトを as const アサーションと組み合わせて定義しましょう。

typescriptconst boxVariants = {
  hidden: {
    opacity: 0,
    scale: 0.8,
  },
  visible: {
    opacity: 1,
    scale: 1,
    transition: { duration: 0.3 },
  },
  exit: {
    opacity: 0,
    scale: 0.8,
    transition: { duration: 0.2 },
  },
} as const satisfies Variants;

as const を使うことで、オブジェクトのプロパティがリテラル型として推論されます。さらに satisfies Variants を付けることで、Motion の Variants 型に準拠していることを保証できます。

ステップ 3:型抽出とエクスポート

定義した variants から型を抽出し、他のファイルでも使えるようにします。

typescript// variants のキー名を型として抽出
type BoxVariantKeys = VariantKeys<typeof boxVariants>;
// 結果: "hidden" | "visible" | "exit"

export { boxVariants };
export type { BoxVariantKeys };

これで、BoxVariantKeys 型は "hidden" | "visible" | "exit" という文字列リテラルの Union 型になります。

Props の型推論を強化する

variants の型定義ができたら、次は motion コンポーネントの props で型推論が効くようにします。

カスタムコンポーネントの型定義

再利用可能なアニメーションコンポーネントを作る際、props の型を厳密に定義することで型安全性が向上します。

typescriptimport { motion, MotionProps } from 'motion/react';
import { ComponentProps } from 'react';

type AnimatedBoxProps = {
  variant: BoxVariantKeys;
  children?: React.ReactNode;
} & Omit<MotionProps, 'variants' | 'initial' | 'animate'>;

この型定義では、variant という props を追加し、その型を先ほど定義した BoxVariantKeys に限定しています。これにより、存在しない variant 名を指定しようとするとコンパイルエラーが発生します。

コンポーネントの実装

型定義をもとに、実際のコンポーネントを実装しましょう。

typescriptexport const AnimatedBox = ({
  variant,
  children,
  ...motionProps
}: AnimatedBoxProps) => {
  return (
    <motion.div
      variants={boxVariants}
      initial='hidden'
      animate={variant}
      exit='exit'
      {...motionProps}
    >
      {children}
    </motion.div>
  );
};

この実装により、<AnimatedBox variant="visible" ​/​> のように使う際、IDE が自動補完で "hidden" | "visible" | "exit" を提示してくれるようになります。

ジェネリクスを活用した汎用的な解決策

特定の variants に依存しない、より汎用的なアプローチも可能です。

ジェネリック型の定義

typescripttype StrictMotionProps<V extends Variants> = {
  variants: V;
  initial?: VariantKeys<V> | boolean;
  animate?: VariantKeys<V>;
  exit?: VariantKeys<V>;
  whileHover?: VariantKeys<V>;
  whileTap?: VariantKeys<V>;
  whileFocus?: VariantKeys<V>;
} & Omit<
  MotionProps,
  | 'variants'
  | 'initial'
  | 'animate'
  | 'exit'
  | 'whileHover'
  | 'whileTap'
  | 'whileFocus'
>;

この StrictMotionProps 型は、variants の型をジェネリクスで受け取り、各アニメーション props の型を variants のキー名に限定します。

汎用コンポーネントの実装

typescriptfunction createStrictMotionComponent<V extends Variants>(
  component: keyof typeof motion
) {
  return function StrictMotion(
    props: StrictMotionProps<V>
  ) {
    const Component = motion[component] as any;
    return <Component {...props} />;
  };
}

このファクトリー関数を使うことで、任意の motion コンポーネントを型安全にラップできます。

下図は、型安全なコンポーネント設計のフローを示しています。

mermaidflowchart TD
  define["Variants 定義<br/>(as const satisfies)"] -->|型抽出| keys["VariantKeys 型"]
  keys -->|適用| props["Props 型定義"]
  props -->|実装| component["型安全な<br/>コンポーネント"]
  component -->|使用時| autocomplete["IDE 自動補完"]
  component -->|誤使用時| error["コンパイルエラー"]

具体例

実践例:モーダルアニメーション

実際のプロジェクトで使える、モーダルコンポーネントの実装例を見ていきましょう。

Variants の定義

typescriptconst modalVariants = {
  hidden: {
    opacity: 0,
    scale: 0.95,
    y: -20,
  },
  visible: {
    opacity: 1,
    scale: 1,
    y: 0,
    transition: {
      type: 'spring',
      stiffness: 300,
      damping: 25,
    },
  },
  exit: {
    opacity: 0,
    scale: 0.95,
    y: 20,
    transition: {
      duration: 0.2,
    },
  },
} as const satisfies Variants;

type ModalVariantKeys = VariantKeys<typeof modalVariants>;

この variants 定義では、モーダルの開閉時のアニメーションを表現しています。hidden から visible へ、そして exit へと状態が遷移します。

背景オーバーレイの Variants

typescriptconst overlayVariants = {
  hidden: {
    opacity: 0,
  },
  visible: {
    opacity: 1,
    transition: { duration: 0.2 },
  },
  exit: {
    opacity: 0,
    transition: { duration: 0.15 },
  },
} as const satisfies Variants;

type OverlayVariantKeys = VariantKeys<
  typeof overlayVariants
>;

オーバーレイは、モーダルの背景として表示される半透明の黒い層です。モーダル本体とは異なる variants を持たせることで、独立したアニメーションを実現できます。

型安全なモーダルコンポーネント

typescriptimport { AnimatePresence } from 'motion/react';

type ModalProps = {
  isOpen: boolean;
  onClose: () => void;
  children: React.ReactNode;
  className?: string;
};

export const Modal = ({
  isOpen,
  onClose,
  children,
  className,
}: ModalProps) => {
  return (
    <AnimatePresence>
      {isOpen && (
        <>
          {/* 背景オーバーレイ */}
          <motion.div
            variants={overlayVariants}
            initial='hidden'
            animate='visible'
            exit='exit'
            onClick={onClose}
            className='modal-overlay'
          />

          {/* モーダル本体 */}
          <motion.div
            variants={modalVariants}
            initial='hidden'
            animate='visible'
            exit='exit'
            className={`modal-content ${className || ''}`}
          >
            {children}
          </motion.div>
        </>
      )}
    </AnimatePresence>
  );
};

この実装では、AnimatePresence を使って、コンポーネントのマウント・アンマウント時にアニメーションを適用しています。initialanimateexit に指定する文字列は、variants で定義したキー名に限定されます。

実践例:リストアニメーション

複数の要素に連続的なアニメーションを適用する、ステージング効果の実装例です。

親子関係を持つ Variants

typescriptconst containerVariants = {
  hidden: {
    opacity: 0,
  },
  visible: {
    opacity: 1,
    transition: {
      staggerChildren: 0.1,
      delayChildren: 0.2,
    },
  },
} as const satisfies Variants;

const itemVariants = {
  hidden: {
    opacity: 0,
    x: -20,
  },
  visible: {
    opacity: 1,
    x: 0,
    transition: {
      type: 'spring',
      stiffness: 300,
      damping: 25,
    },
  },
} as const satisfies Variants;

type ContainerVariantKeys = VariantKeys<
  typeof containerVariants
>;
type ItemVariantKeys = VariantKeys<typeof itemVariants>;

staggerChildren を使うことで、子要素のアニメーションを順次実行できます。delayChildren は、最初の子要素のアニメーション開始を遅らせる設定です。

リストコンポーネントの実装

typescripttype AnimatedListProps<T> = {
  items: T[];
  renderItem: (item: T, index: number) => React.ReactNode;
  className?: string;
};

export function AnimatedList<T>({
  items,
  renderItem,
  className,
}: AnimatedListProps<T>) {
  return (
    <motion.ul
      variants={containerVariants}
      initial='hidden'
      animate='visible'
      className={className}
    >
      {items.map((item, index) => (
        <motion.li key={index} variants={itemVariants}>
          {renderItem(item, index)}
        </motion.li>
      ))}
    </motion.ul>
  );
}

このコンポーネントは、ジェネリクス <T> を使って任意の型のリストをサポートしています。親要素の containerVariants"visible" 状態になると、子要素の itemVariants が順次 "visible" に遷移します。

使用例

typescripttype Task = {
  id: string;
  title: string;
  completed: boolean;
};

const tasks: Task[] = [
  {
    id: '1',
    title: 'TypeScript の型定義を学ぶ',
    completed: true,
  },
  {
    id: '2',
    title: 'Motion のアニメーションを実装',
    completed: false,
  },
  { id: '3', title: '記事を執筆する', completed: false },
];

function TaskList() {
  return (
    <AnimatedList
      items={tasks}
      renderItem={(task) => (
        <div className='task-item'>
          <input
            type='checkbox'
            checked={task.completed}
            readOnly
          />
          <span>{task.title}</span>
        </div>
      )}
    />
  );
}

この例では、タスクリストの各項目が 0.1 秒ずつ遅れてアニメーションします。型安全性が保たれているため、renderItem 関数内で task オブジェクトのプロパティに安全にアクセスできます。

実践例:条件付き Variants

状態に応じて異なるアニメーションを適用するパターンも、型安全に実装できます。

多状態 Variants の定義

typescriptconst buttonVariants = {
  idle: {
    scale: 1,
    backgroundColor: '#3b82f6',
  },
  hover: {
    scale: 1.05,
    backgroundColor: '#2563eb',
    transition: { duration: 0.2 },
  },
  pressed: {
    scale: 0.95,
    backgroundColor: '#1d4ed8',
  },
  loading: {
    scale: 1,
    backgroundColor: '#93c5fd',
    transition: {
      repeat: Infinity,
      duration: 1,
    },
  },
  disabled: {
    scale: 1,
    backgroundColor: '#9ca3af',
    cursor: 'not-allowed',
  },
} as const satisfies Variants;

type ButtonVariantKeys = VariantKeys<typeof buttonVariants>;

このボタンは、通常・ホバー・押下・ローディング・無効化の 5 つの状態を持ちます。

状態管理と連携したコンポーネント

typescripttype AnimatedButtonProps = {
  children: React.ReactNode;
  onClick?: () => void;
  isLoading?: boolean;
  isDisabled?: boolean;
  className?: string;
};

export const AnimatedButton = ({
  children,
  onClick,
  isLoading = false,
  isDisabled = false,
  className,
}: AnimatedButtonProps) => {
  // 現在の状態を判定
  const getCurrentVariant = (): ButtonVariantKeys => {
    if (isDisabled) return 'disabled';
    if (isLoading) return 'loading';
    return 'idle';
  };

  return (
    <motion.button
      variants={buttonVariants}
      initial='idle'
      animate={getCurrentVariant()}
      whileHover={
        !isDisabled && !isLoading ? 'hover' : undefined
      }
      whileTap={
        !isDisabled && !isLoading ? 'pressed' : undefined
      }
      onClick={
        !isDisabled && !isLoading ? onClick : undefined
      }
      disabled={isDisabled || isLoading}
      className={className}
    >
      {isLoading ? 'Loading...' : children}
    </motion.button>
  );
};

getCurrentVariant 関数の戻り値の型が ButtonVariantKeys に限定されているため、存在しない variant 名を返すことができません。これにより、実装ミスを防げます。

下図は、条件分岐を含むアニメーション状態遷移を示しています。

mermaidstateDiagram-v2
  [*] --> idle: 初期化
  idle --> hover: マウスオーバー
  hover --> idle: マウスアウト
  hover --> pressed: クリック
  pressed --> idle: リリース
  idle --> loading: 処理開始
  loading --> idle: 処理完了
  idle --> disabled: 無効化
  disabled --> idle: 有効化

図で理解できる要点:

  • ボタンは 5 つの状態(idle, hover, pressed, loading, disabled)を持つ
  • ユーザー操作や内部状態によって状態が遷移する
  • 各状態遷移は型安全に管理される

まとめ

本記事では、Motion(旧 Framer Motion)と TypeScript を組み合わせる際の Variant 型定義と Props 推論の強化方法をご紹介しました。

主なポイントは以下の通りです。

#項目内容
1Variants の型定義as const satisfies Variants を使い、リテラル型として定義する
2型抽出VariantKeys<T> ユーティリティ型でキー名を Union 型として抽出
3Props の型安全化カスタムコンポーネントで variant 名を型として限定する
4ジェネリクスの活用汎用的な型定義で、複数の variants に対応できる
5実践的な実装モーダル、リスト、ボタンなど実用的なパターンを適用

これらのテクニックを活用することで、以下のメリットが得られます。

開発体験の向上: IDE の自動補完やリファクタリング機能が最大限に活かせるようになり、コーディング効率が大幅に改善されます。

バグの早期発見: 存在しない variant 名の指定やタイポをコンパイル時に検出できるため、実行時エラーのリスクが減少します。

保守性の向上: 型定義によってコードの意図が明確になり、チーム開発やコードレビューがスムーズになるでしょう。

Motion のアニメーション機能は非常に強力ですが、TypeScript の型システムと適切に組み合わせることで、さらに安全で保守性の高いコードベースを構築できます。ぜひこれらのパターンをプロジェクトに取り入れて、型安全なアニメーション実装を体験してみてください。

関連リンク