グローバルナビゲーションへ

本文へ

フッターへ

お役立ち情報Blog



TypeScriptの型定義がない外部ライブラリとmonorepoを組み合わせた時にTypeScriptのコンパイルエラーが発生して困ったこと

当社では CSS スタイリングではCSS Modulesを、日付選択(Datepicker)のライブラリにReact DayPickerを使用しています。

React DaypickerではCSSの型宣言はされているのですが、CSS Modulesの型定義ファイルがないため独自にCSSのスタイルを当てるにはひと手間が必要になります。

参照:https://github.com/gpbl/react-day-picker/tree/main/src

今回はこのCSS Modulesの型定義とmonorepoを組み合わせた時にTypeScriptのコンパイルエラーが発生して困ったこととその対処方法についての共有です。

アンビエント宣言(ambient declarations)

まずはReact DaypickerのCSS Modulesの型定義ファイルを作成すればよさそうです。

参照:https://www.typescriptlang.org/docs/handbook/declarations-files/templates/module-d-ts.html

The most common case for learning how .d.ts files work is that you’re typing an npm package with no types. In that case, you can jump straight to Modules .d.ts

.d.ts ファイルがどのように機能するかを学ぶための最も一般的なケースは、npm パッケージの型を入力する場合です。 その場合は、そのままモジュール.d.ts にジャンプできます。
(DeepL 翻訳)

公式ドキュメントでもその通りの使用例が書かれています。

参照:https://www.typescriptlang.org/docs/handbook/declarations-files/templates/module-d-ts.html

declareキーワードを使うことでTypeScriptのコンパイラに対象コードの型を伝えることができるわけですね。

公式ドキュメントからは記載を見つけられませんでしたが、このような型付けの方法を一般的にアンビエント宣言(ambient declarations)と呼ばれているようです。

外部ライブラリにアンビエント宣言でTypeScriptの型を定義する

React Daypickerの公式にCSS Modulesの使用例があります。

import { DayPicker } from "react-day-picker";
import classNames from "react-day-picker/style.module.css";

console.log(classNames); // Output the class names as parsed by CSS modules.

export function MyDatePicker( {
      return <DayPicker mode="single" classNames={classNames} />;
})

参照:https://daypicker.dev/docs/styling#importing-the-css-module

この例のconsole.logの出力結果がこちらです。

{
  root: 'style_root__YirYh',
  day: 'style_day__GZMEe',
  dayButton: 'style_day_button__O8LLc',
  captionLabel: 'style_caption_label__ABcGQ',
  buttonNext: 'style_button_next__al7zP',
  buttonPrevious: 'style_button_previous__5B_f9',
  chevron: 'style_chevron__60RGI',
  nav: 'style_nav__ZUeps',
  dropdowns: 'style_dropdowns__D2F_j',
  dropdown: 'style_dropdown__boRHO',
  dropdownRoot: 'style_dropdown_root__GFlYP',
  monthCaption: 'style_month_caption__6kngE',
  months: 'style_months__tmq4Y',
  weekday: 'style_weekday__BUgyG',
  weekNumber: 'style_week_number__zWxLy',
  weekNumberInteractive: 'style_week_number_interactive__9AiAo',
  today: 'style_today__69_YV',
  outside: 'style_outside__VBnb9',
  selected: 'style_selected__4EkPh',
  disabled: 'style_disabled__hWC1e',
  hidden: 'style_hidden__yiSah',
  rangeStart: 'style_range_start__Y2tWy',
  rangeMiddle: 'style_range_middle__vZpbk',
  rangeEnd: 'style_range_end__Qyzg9',
  focusable: 'style_focusable__JKrJp'
}

※CSS ModulesなのでReact Daypickerが提供しているCSSの型定義ファイルとは構造が違っています

参照:https://github.com/gpbl/react-day-picker/blob/main/src/style.css.d.ts

このCSS Modulesの出力された構造に合わせて型宣言をすればよさそうです。

愚直に書くなら各プロパティに値の型にstring型を指定すればよさそうですが、プロパティ名が必要になることはなさそうなのでRecord型で定義してしまいます。

// react-day-picker.d.ts
declare module "react-day-picker/style.module.css" {
  export default Record<string, string>;
}

この型定義ファイルを配置すると、TypeScriptのコンパイルエラーを回避することができました。

monorepo と組み合わせると動作しない

ここから今回の主題です。

例になりますが以下のようなディレクトリ構成で、monorepo内の依存ライブラリの利用はバンドル後のファイルではなくpackage.jsonexportsフィールドを使用して直接参照するような構成を取っています。

uiパッケージでの TypeScript のコンパイルエラーは解消しましたが、そのパッケージを使用するapp1でTypeScriptのコンパイルエラーが発生していました。

src/
├── apps
│   ├── app1 // packages/ui を使用するアプリケーション
│   └── app2
└── packages
    └── ui    // React Daypicker を使用しているパッケージ

app1ではtsconfig.jsonincludesプロパティにuiパッケージのパスを指定していないので、uiパッケージ内に作成したreact-day-picker.d.tsファイルをTypeScriptコンパイラが読み込んでおらず、型定義がされていないと解釈されてしまうためだと思われます。

対処方法としてまず考えたのがuiパッケージを使用するパッケージで都度同じreact-day-picker.d.tsファイルを作成することですが、使うたびに型定義ファイルを作成しないといけないため使い勝手が非常によろしくありません。

できることならuiパッケージの型定義はuiパッケージ内で完結させたいところです。

ただreact-day-picker.d.tsのような型定義ファイルを外部エクスポートする方法が分からず、package.jsonexportsフィールドで指定してみたりと色々試してみましたが中々うまくいきません。

対処方法

+ import "./react-day-picker";
  import { DayPicker } from "react-day-picker";
  import classNames from "react-day-picker/style.module.css";

  console.log(classNames); // Output the class names as parsed by CSS modules.

  export function MyDatePicker( {
        return <DayPicker mode="single" classNames={classNames} />;
  })

結論からいうと、React DaypickerのCSS Modulesを使用するコンポーネントで型定義ファイルをimportするだけでした。

tsconfig.jsonの設定でTypeScriptが暗黙的に読み込んでいたreact-day-picker.d.tsを、import宣言によって別のパッケージから読み込まれた時にもTypeScriptコンパイラが型定義ファイルを解釈できるようになったのだと思います。

名前付きインポートや、デフォルトのインポート、名前空間のインポートなどは普段使っていたのでimport宣言についてある程度理解していると思っていましたが、fromを使用しないimport宣言に辿り着くまでに時間が掛かりました。

思い返せば純粋なCSSでCSSスタイリングをする場合なども、同じようにfromを使用せずにimport宣言を行っていますね。

まとめ

色々なimport宣言の方法についてはMDNのドキュメントが参考になりました。

参照:https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Statements/import

副作用のためだけにモジュールをインポートしたいユースケースが、まさに今回の「副作用の import」に当てはまります。

import 宣言には4つの形式があります:

  • 名前付き import: import { export1, export2 } from “module-name”;
  • デフォルトの import: import defaultExport from “module-name”;
  • 名前空間の import: import * as name from “module-name”;
  • 副作用の import: import “module-name”;

名前付きimport

import { Foo } from "./foo";

デフォルトのimport

import Foo from "./foo";

名前空間のimport

import * as Foo from "./foo";

副作用のimport

import "./foo";

今回はReact Daypickerを例にご紹介しましたが、他の型宣言のない外部ライブラリでも応用が効きます。
型宣言のない外部ライブラリを使用してTypeScriptのコンパイルエラーが発生した際にはお試しください。

それにしても私はTypeScriptを雰囲気で使っている。

この記事を書いた人

美髭公
美髭公事業開発部 web application engineer
2013年にアーティスに入社。システムエンジニアとしてアーティスCMSを使用したWebサイトや受託システムの構築・保守に携わる。環境構築が好き。
この記事のカテゴリ

FOLLOW US

最新の情報をお届けします