TypeScript 5.1のJSX.ElementTypeと各種ライブラリでの対応 @types/reactとか@emotion/reactとか

こんな感じのエラーを "@types/react": "^17.0" を指定せずに倒せるようになった!!!

Type error: 'SomeComponent' cannot be used as a JSX component.
  Its return type 'ReactNode' is not a valid JSX element.
    Type 'undefined' is not assignable to type 'Element | null'.

まとめ

一番最初に対応方法のまとめを書いておく

  • jsxImportSource を弄っていない場合
    • typesciprt を 5.1、@types/react を 18.2.8 にアップデートする
  • jsxImportSource を弄っている場合
    • typesciprt を 5.1、@types/react を 18.2.8 にアップデートする
    • JSX.ElementType を宣言する

例えば2023年6月4日時点で @emotion/react を使っている場合、アップデートを行った後に以下のように宣言する。

// src/types/emotion-react-jsx-runtime.d.ts
import { JSX } from '@emotion/react/jsx-runtime';

declare module '@emotion/react/jsx-runtime' {
  namespace JSX {
    type ElementType = React.JSX.ElementType;
  }
}

TypeScript 5.1 JSX.ElementType

TypeScript 5.1がリリースされた。この新機能の一つとして、JSXのElementとして妥当かどうか確かめるための新しい型 JSX.ElementType の存在が導入された。

これまで、関数コンポーネントは null | JSX.Element を返すことが強制されていた。例えば、const Component = () => 42; <Component /> という例での Component は42を返し、これは null | JSX.Element には該当しないため、型チェックの段階で弾かれる。

ただ、Reactで作成する関数コンポーネントは ReactNode を返す。この型はおおよそ Element | number | string | Iterable<ReactNode> | undefined (将来的には Promise<ReactNode>) のようになっており、上記で記した型と一致しない。ここで、ReactNode も問題なくコンポーネントとして利用できるようにしたいモチベーションが発生する。

この解決策として、TypeScript 5.1以降より、TypeScriptのjsx周辺で型 JSX.ElementType を使うようになった。各種 jsx-runtime を提供するライブラリは JSX.ElementType を埋めるように宣言をする必要がある。JSX.ElementType を使ってコンポーネントを検査した結果、パスしたのであれば、そのコンポーネントはそのランタイムで使用可能だということが分かる、という流れ。例えばReactでこれを用意するなら以下のような感じになる。

// inlined `React.JSXElementConstructor`
type ReactJSXElementConstructor<Props> =
  | ((props: Props) => React.ReactNode)
  | (new (props: Props) => React.Component<Props, any>);

declare global {
  namespace JSX {
    type ElementType = string | ReactJSXElementConstructor<any>;
  }
}

stringや、ReactNode を返す関数コンポーネントや、クラスコンポーネントに対応している型を用意し、JSX.ElementType として宣言する。これで、ReactのJSXランタイムは、Reactで作成するような関数コンポーネントをそのまま扱えるようになった。

github.com

@types/react 18.2.8 JSX.ElementType

実際に、@types/react 18.2.8 のリリースでは、コンポーネントの返り値として ReactNode を直接利用できるような ElementType を宣言するようになった。抜粋するとこんなかんじ。

type JSXElementConstructor<P> =
  | ((props: P, deprecatedLegacyContext?: any) => ReactNode)
  | (new (props: P) => Component<any, any>);

namespace JSX {
  type ElementType = string | React.JSXElementConstructor<any>;
}

index.d.ts

github.com

怒涛のCloses

Emotionで JSX.ElementType に対応してみる

追記: 2023年6月7日に下部のセクションで出したPRがマージされたので、@emotion/react がv11.11.1より大きければ既に対応されているはず!!

これで万事解決かと思い、自分のNext.jsのプロジェクト内の typescript5.1.3 に、@types/react18.2.8 にアップグレードしたが、変わらず同じ問題が発生した。端的に言うと、Emotionが関連した問題だった。

今回のプロジェクトではEmotionを使用している。Emotionは tsconfig.jsonjsxImportSource に対して @emotion/react などと指定している。

@emotion/react では、コンポーネントのpropとして新規に css が指定できるようになっている。この解決のため、任意の方法でjsxのランタイムを差し替え、新規に css propを指定できるように変更が入っている。差し替え方法の一例としては上記のように、jsxImportSource@emotion/react を指定する。これによってjsxのランタイムとして @emotion/react/jsx-runtime が利用されるようになる。

ランタイム内では、基本的にはReactのランタイムのJSXをそのまま利用しているが、IntrinsicElements に対しては追加で手を入れており、これによって css prop の利用を実現している。

// 事前に `type ReactJSXElement = JSX.Element` みたいにReactが宣言した型を `ReactXXXX` の型に再代入している

// ↓後に JSX という名前でexportされる
export namespace EmotionJSX {
  interface Element extends ReactJSXElement {}
  interface ElementClass extends ReactJSXElementClass {}
  interface ElementAttributesProperty
    extends ReactJSXElementAttributesProperty {}
  interface ElementChildrenAttribute extends ReactJSXElementChildrenAttribute {}

  type LibraryManagedAttributes<C, P> = WithConditionalCSSProp<P> &
    ReactJSXLibraryManagedAttributes<C, P>

  interface IntrinsicAttributes extends ReactJSXIntrinsicAttributes {}
  interface IntrinsicClassAttributes<T>
    extends ReactJSXIntrinsicClassAttributes<T> {}

  // ここで `css` prop を使えるようにしている!
  type IntrinsicElements = {
    [K in keyof ReactJSXIntrinsicElements]: ReactJSXIntrinsicElements[K] & {
      css?: Interpolation<Theme>
    }
  }
}

このあたり

ここで問題なのは、@emotion/react/jsx-runtime が提供するJSXには、2023年6月4日時点では ElementType が宣言されていないということだった。ただ、先程提示した通り、基本的にはReactが宣言した型をそのまま使いまわしているだけなので、追加で ElementType を宣言してやれば良い。もちろん、事前に typescript を 5.1 に、 @types/react を 18.2.8 にアップデートしておく必要がある。

// Emotionが定義したJSXをそのまま持ってくる
import { JSX } from '@emotion/react/jsx-runtime';

declare module '@emotion/react/jsx-runtime' {
  namespace JSX {
    // 追加で `ElementType` を宣言する
    type ElementType = React.JSX.ElementType;
  }
}

Emotion側に対応してもらいたい

追記: マージされた!!!!!!v11.11.1からは素の状態で対応されているはず。

せっかくならEmotion側で対応してほしい。

2023年6月4日時点の @emotion/react からの @types/react への依存状況はオプショナルなものになっており、しかもバージョンの指定も存在しない。なので、18.2.8以上に依存することがないように ElementType をライブラリ内で宣言するような形で対応するPRを出した。

github.com

正直、JS関連のライブラリに対する文化などを知らないので、これが歓迎されるものなのかはわからないが、マージされればいいな~くらいの気持ち。マージされたら最初に書いた対応も必要なくなりそう。