LoginSignup
332
247

More than 5 years have passed since last update.

TypeScriptの型におけるJSXサポートが100%分かる記事

Last updated at Posted at 2019-03-28

TypeScriptはJavaScriptに静的型を付けることができるAltJSです。2015年9月に登場したTypeScript 1.6ではJSXのサポートが搭載され、.tsxという拡張子を用いることでJSXを含むコードを書いたり型チェックしたりすることができます。

JSXはJavaScriptに対してHTML(あるいはXML)のタグのような構文を導入する拡張記法です。以下の例のようにJavaScriptプログラム中に式としてタグを書くことができます(https://facebook.github.io/jsx/ から引用):

// Using JSX to express UI components.
var dropdown =
  <Dropdown>
    A dropdown list
    <Menu>
      <MenuItem>Do Something</MenuItem>
      <MenuItem>Do Something Fun!</MenuItem>
      <MenuItem>Do Something Else</MenuItem>
    </Menu>
  </Dropdown>;

render(dropdown);

この例では<Dropdown>...</Dropdown>の部分がひとつの式であり、dropdown変数に代入されています。JSXの使い道として最もよく知られているのはReactでしょう。ReactはUIライブラリであり、ちょうど上の例のようにJSXを使って構築したUIをレンダリングすることができます。

この記事のテーマはJSXと型の関係です。TypeScriptでは、JSX部分に対してもちゃんと型チェックを行ってくれるのが特徴的です。再びReactを例に用います。

import * as React from 'react';

const MyComponent: React.FunctionComponent<{
  foo: number;
  bar: string;
}> = ({foo, bar}) => <p>My component {foo} {bar}</p>;

const elm = <MyComponent
  foo="abc" // ←ここで型エラー(fooには文字列ではなく数値を渡さないといけないので)
  bar="def"
/>;

このコードでは、MyComponentを2つのprops, foobarを受け取るコンポーネントとして型を定義しています。下半分でMyComponentをコンポーネントとしてJSXから使用しています。このようにHTMLの属性のような記法でpropsをコンポーネントに渡すことができます。このコードではfoobarも文字列を渡しており、MyComponentの型と違っているのでエラーとなります。

ほとんどの場合TypeScriptのJSXサポートはReactと組み合わせて使用しますが、React以外にもJSXと組み合わせられるライブラリは存在しており(preactとか)、TypeScriptはそれらと一緒に使うことも可能です。それゆえに、TypeScriptのJSXサポートはReact専用のものではなくJSXという記法に対して一般化されたものとなっています。Reactやpreact用の型定義は、実はTypeScriptが型の上で提供する特別なインターフェースを用いることで、JSXに対する適切な型付けを実現しています。

この記事では、JSXの型チェックのためにTypeScriptから提供されているインターフェースについて解説します。この記事を完全に理解することで、@types/reactのようなJSXサポートを含む型定義を書けるようになるでしょう。

この記事の内容の大部分はTypeScriptハンドブック日本語訳した人もいるようです)に書いてある内容ですが、より分かりやすく噛み砕いて説明して理解率100%を目指します。

組み込み要素

いきなりですが、次のコードは組み込み要素を定義するコード例です。

declare namespace JSX {
    interface IntrinsicElements {
        foo: {
            hoge: string;
            fuga: number;
        }
    }
}

const elm = <foo hoge="文字列" fuga={123} />;

このコードではJSXという名前空間を宣言し、その中のIntrinsicElementsインターフェースを定義しています。実は、このJSXという名前空間がTypeScriptが提供するJSXサポートの要です。この中に特定の名前のインターフェースを定義することによって、TypeScriptへJSXの型チェックに関する指示を出すことができます。

JSX名前空間の中にIntrinsicElementsというインターフェースを作ることで組み込み要素を定義します。今回このインターフェースはfooというプロパティのみ持っているため、今回存在する組み込み要素はfooだけです。なお、組み込み要素というのは名前が小文字の要素で、ReactではHTML要素(<div>とか<span>とか)に相当します。

各プロパティの型をオブジェクトとすることで、その組み込み要素に渡すことができる属性(props)を指定します。この場合、foo要素にはhogeという属性を文字列で、そしてfugaという属性を数値で与えなければいけません。以下のようなものはエラーとなります。

// エラー(hoge属性の型が違うので)
const elm2 = <foo hoge={123} fuga={456} />
// エラー(barという組み込み要素は存在しないので)
const elm3 = <bar />;

JSX式の結果の型

そもそも、JSX式の結果は何でしょうか。つまり、例えば以下の変数elmの型は何になるのでしょうか。

const elm = <foo hoge="文字列" fuga={123} />;

調べてみると、elmの型はJSX.Elementです。お察しの通り、上のJSX名前空間の下にElement型を定義してやることで、JSX式の型を自分で定義できるのです。

例えば、思い切ってJSX式の結果をstring型にしてみましょう。すると、elmの型がstringになります。

なお、以降の例も単体でコンパイルできるようにdeclare namespace JSX全体の記載がありますが、前回との変更点はコメントで明記しますので全部読みなおさなくても大丈夫です。

declare namespace JSX {
    // JSX.Elementを定義
    type Element = string;
    interface IntrinsicElements {
        foo: {
            hoge: string;
            fuga: number;
        }
    }
}

// elmの型はstring
const elm = <foo hoge="文字列" fuga={123} />;

JSX式が直接HTML文字列になるおもしろいテンプレートエンジンを作りたくなったときはこのような型定義が役に立つでしょう。

なお、どんな要素でもJSX式の結果はJSX.Elementです。<foo /><bar />で結果が変わるというようなものは作ることができません。

子要素の型

いままで出てきたfoo要素は全部最後が/>で終わっていした。これはXMLなどでも見られる記法で、中身が何もないときに開始タグと終了タグをまとめて書ける記法です。

しかし、JSXでは要素が子要素を持つこともできます。今の状態では、子要素には何でも入れることができます。

declare namespace JSX {
  // Elementの型を変更した(文字列は都合が悪いので)
  type Element = {
    this_is_element: true;
  };
  interface IntrinsicElements {
    foo: {
      hoge: string;
      fuga: number;
    };
    // bar要素を追加した
    bar: {};
  }
}

const elm = (
  <foo hoge="文字列" fuga={123}>
    <bar>文字列</bar>
  </foo>
);

この場合、foo要素の子は1つのbar要素であり、bar要素の子は1つの文字列です。

実は、要素の子がどうなるべきかを型で制限することが可能です。そのためには2つのことを行う必要があります。

まずJSX.ElementChildrenAttributeという特殊なインターフェースを定義します。これはただ1つのプロパティを持つインターフェースであり、そのプロパティ名が子を表すプロパティ名として宣言されます。

そして、各要素の属性の宣言時にそのプロパティ名を用いて子の型を指定します。

実際にやってみると次のようになります。以下ではchildrenという名前を子を表すプロパティ名として宣言しました(これはReactと同じです)。

declare namespace JSX {
  interface ElementChildrenAttribute {
    // childrenという名前を子を表すプロパティ名として宣言
    children: any;
  }
  type Element = {
    this_is_element: true;
  };
  interface IntrinsicElements {
    foo: {
      hoge: string;
      fuga: number;
      // childrenプロパティで子の型を指定
      // (foo要素の子は別の要素)
      children: JSX.Element;
    };
    bar: {
      // barの子は文字列
      children: string;
    };
  }
}

const elm = (
  <foo hoge="文字列" fuga={123}>
    <bar>文字列</bar>
  </foo>
);

// エラー(fooの子が別の要素ではなく文字列になっているので)
const elm2 = (
  <foo hoge="文字列" fuga={123}>
    文字列
  </foo>
);
// エラー(barの子が文字列ではなく数値になっているので)
const elm3 = <bar>{456}</bar>;
// エラー(barの子が無いので)
const elm4 = <bar />;

お察しの通り、JSX式で書かれた各要素に対してはその要素に指定された属性たちを集めたオブジェクト型が作られます。例えば<bar>文字列</bar>に対しては、属性は何も無くて子が文字列なので{ children: string; }というオブジェクト型になります。そしてこれがJSX.IntrinsicElements.barに合致する(代入可能)かが調べることによって型チェックが行なわれます。

<bar />のようにbarの子が無い場合は、属性も無く子も無いので{}型になります。これはJSX.IntrinsicElements.bar、すなわち{ children: string; }には代入できないので型エラーとなります。

<foo hoge="文字列" fuga={123}>文字列</foo>のように属性がある場合も同様です。この場合与えられた属性たちの型は{ hoge: string; fuga: number; children: string; }となります。これはJSX.IntrinsicElements.fooとは合致しない(childrenの型が違う)ので型エラーとなります。

このことが分かれば、要素の宣言において省略可能な子や省略可能な属性を宣言することができます。次の例ではfoobarの属性や子要素の型をいじってみました。

declare namespace JSX {
  interface ElementChildrenAttribute {
    children: any;
  }
  type Element = {
    this_is_element: true;
  };
  interface IntrinsicElements {
    foo: {
      // fooの2つの属性を省略可能にした
      hoge?: string;
      fuga?: number;
      // childrenプロパティは文字列でも別の要素でもOK
      children: JSX.Element | string;
    };
    bar: {
      // barの子は何でもOK
      children?: unknown;
    };
  }
}

// 以下は全部OK
const elm = (
  <foo>
    <bar>文字列</bar>
  </foo>
);
const elm2 = (
  <foo hoge="文字列" fuga={123}>
    文字列
  </foo>
);
const elm3 = <bar>{456}</bar>;
const elm4 = <bar />;

JSXと子要素の型の対応

これまで見たように、JSX内に出現した文字列はstring型の子として扱われます。例えば<bar>foobar</bar>の場合bar要素の子としてstring型の値すなわち文字列が渡されます。

一方、<bar>{456}</bar>の場合はbar要素の子はnumber型でした。JSX内の{}という構文はその中身(通常のJavaScriptの式)をそのまま子として渡すという意味です。これにより任意のものを子とすることができます。

実は要素の子はもう1パターンあります。それは次の例のように子が複数ある場合です。

const elm = (
  <foo>
    <bar>子1</bar>
    子2
    <bar />
  </foo>
);

この場合、fooに渡されるchildren配列となります。配列の要素はもちろんそれぞれの子要素です。

次のような定義にすることで、子として配列が渡された場合も対応することができますね。

declare namespace JSX {
  interface ElementChildrenAttribute {
    children: any;
  }
  type Element = {
    this_is_element: true;
  };
  interface IntrinsicElements {
    foo: {
      hoge?: string;
      fuga?: number;
      // childrenプロパティに配列も許可
      children: JSX.Element | string | Array<JSX.Element | string>;
    };
    bar: {
      children?: unknown;
    };
  }
}

const elm = (
  <foo>
    <bar>子1</bar>
    子2
    <bar />
  </foo>
);

以上で、組み込み要素をどうやって定義するのかの話が終わりました。このように、JSX.IntrinsicElementsインターフェースに要素を定義してやることで、どんな要素が存在し、それぞれがどんな属性を受け入れるのかを定義することができます。TypeScriptはそれに基づいて型チェックを行ってくれます。

ユーザー定義コンポーネント

JSXでは組み込み要素のほかに、ユーザーが定義したコンポーネントを使用することができます。次はこれに対する型チェックを見ていきます。ユーザー定義コンポーネントは、JSX式では名前が大文字で始まるタグとして表れます。あらかじめタグ名と同名の変数に当該コンポーネントが入っている必要があります。以下はReactにおけるユーザー定義コンポーネントの例です。

Reactでのユーザー定義コンポーネントの例
class MyComponent extends React.Component {
  render() {
    return <div>my component</div>;
  }
}
const elm = <MyComponent />;

ではTypeScriptとJSXの話に戻ります。前提として、デフォルトでは関数及びクラスがユーザー定義コンポーネントとして使用可能です。

declare namespace JSX {
  interface ElementChildrenAttribute {
    children: any;
  }
  type Element = {
    this_is_element: true;
  };
  interface IntrinsicElements {
    bar: {
      children?: unknown;
    };
  }
}

class MyComponent1 {}
const MyComponent2 = () => <bar />;

const elm1 = <MyComponent1 />;
const elm2 = <MyComponent2 />;

関数コンポーネント

まず、ユーザー定義の関数コンポーネントから見ていきます。関数をコンポーネントとして使用する場合はひとつ条件があります。それは、返り値の型がJSX.Element(に代入可能)でなければならない点です。これに当てはまらない関数はコンポーネントとして使用できません。

const NotComponent = () => 123;
// エラー(NotComponentの返り値がJSX.Elementではないので)
const elm3 = <NotComponent />;

次に、コンポーネントである以上、属性を受け取ることができます。関数コンポーネントが受け取れる属性はどのように決まるのでしょうか。これは単純で、関数の引数の型が受け取れる属性一覧の型となります。上のMyComponent2は引数が無かったので、何も属性を受け取りません。では、引数を受け取る関数コンポーネントを作ってみましょう。

declare namespace JSX {
  interface ElementChildrenAttribute {
    children: any;
  }
  type Element = {
    this_is_element: true;
  };
  interface IntrinsicElements {
    bar: {
      children?: unknown;
    };
  }
}

// MyFunctionComponentの引数の型
interface MyFunctionComponentProps {
  foo: string;
  children?: JSX.Element | string | Array<JSX.Element | string>;
}
// MyFunctionの定義
const MyFunctionComponent = (props: MyFunctionComponentProps) => {
  return (
    <bar>
      {props.foo} {props.children}
    </bar>
  );
};
// 使用例
const elm = <MyFunctionComponent foo="123">child</MyFunctionComponent>;

MyFunctionComponentの定義に注目すると、引数の型がMyFunctionComponentPropsであると宣言しています。これが受け取れる属性の一覧として採用されます。一覧の書き方は組み込み要素のときと同じです。今回の場合、MyFunctionComponentは文字列のfoo属性を持ち、子を受け取ることも可能です。

クラスコンポーネント

次にクラスコンポーネントです。先ほど見たように、デフォルトの状態では任意のクラスをクラスコンポーネントとして利用可能です。しかし、これは望ましくないことが多いでしょう。そこで、どんなクラスがクラスコンポーネントとして利用可能かを制限することができます。そのためには、JSX.ElementClassを定義します。その場合、インスタンスの型がJSX.ElementClassに当てはまるクラスがクラスコンポーネントとして利用可能になります1

declare namespace JSX {
  interface ElementChildrenAttribute {
    children: any;
  }
  type Element = {
    this_is_element: true;
  };
  // ElementClassを定義
  interface ElementClass {
    render: () => any;
  }
  interface IntrinsicElements {
    bar: {
      children?: unknown;
    };
  }
}

class NotClassComponent {}
class ClassComponent {
  render() {
    return <bar />;
  }
}

// ClassComponentはクラスコンポーネントとして利用可能
const elm = <ClassComponent />;
// エラー(NotClassComponentはクラスコンポーネントではない)
const elm2 = <NotClassComponent />;

この例では、JSX.ElementClassは「renderメソッドを持つオブジェクトの型」です。よって、インスタンスがその条件を満たすようなクラスがクラスコンポーネントとなります。実際、NotClassComponentのインスタンスはrenderメソッドを持ちませんがClassComponentのインスタンスはrenderメソッドを持ちます。よって、NotClassComponentはクラスコンポーネントとして使用できない一方でClassComponentはクラスコンポーネントとして使用可能です。

続いて、クラスコンポーネントの属性の指定方法です。これは2通りあり、1つ目は関数コンポーネントと似ています。クラスのコンストラクタの引数の型が受け取れる属性たちの型となります。

declare namespace JSX {
  interface ElementChildrenAttribute {
    children: any;
  }
  type Element = {
    this_is_element: true;
  };
  interface ElementClass {
    render: () => any;
  }
  interface IntrinsicElements {
    bar: {
      children?: unknown;
    };
  }
}

// ClassComponentが受け取れる属性の型を定義
interface MyProps {
  hoge: string;
}
class ClassComponent {
  constructor(props: MyProps) {}
  render() {
    return <bar />;
  }
}

// ClassComponentはhoge属性を受け取る
const elm = <ClassComponent hoge="123" />;

もう一つの方法は、インスタンスの特定のプロパティの型を見よとする方法です。どのプロパティを見るべきかはJSX.ElementAttributesPropertyインターフェースの唯一のプロパティとして宣言します。これはJSX.ElementChildrenAttributeを用いて子を表す属性を定義したのと似ていますね。

declare namespace JSX {
  interface ElementChildrenAttribute {
    children: any;
  }
  // クラスコンポーネントの属性はインスタンスの`props`プロパティの型を見る
  interface ElementAttributesProperty {
    props: any;
  }
  type Element = {
    this_is_element: true;
  };
  interface ElementClass {
    render: () => any;
  }
  interface IntrinsicElements {
    bar: {
      children?: unknown;
    };
  }
}

interface MyProps {
  hoge: string;
}
class ClassComponent {
  // インスタンスのpropsプロパティの型が属性のプロパティとなる
  // (行儀が悪いけど今回は型のみ宣言)
  public props!: MyProps;
  render() {
    return <bar />;
  }
}

// ClassComponentはhoge属性を受け取る
const elm = <ClassComponent hoge="123" />;

以上でクラスコンポーネントの型チェックが分かりました。JSX.ElementClassを用いてクラスコンポーネントとして用いることができるクラスを制限し、クラスコンポーネントが受け取る属性は今回紹介した2種類の方法で指定します。

追加の属性を定義する

これまで見たように、属性の定義は組み込み要素・関数コンポーネント・クラスコンポーネントでそれぞれ異なっていました。実は、コンポーネントに渡すことができる属性を定義する別の方法が3種類あります。

JSX.IntrinsicAttributes

1つ目はJSX.IntrinsicAttributesです。このインターフェースに定義された属性は、全ての関数コンポーネント・クラスコンポーネントに対して適用されます。例えばReactではkey属性が全てのコンポーネントで使用することができ、これに当てはまります2。では例を見ましょう。

declare namespace JSX {
  interface ElementChildrenAttribute {
    children: any;
  }
  interface ElementAttributesProperty {
    props: any;
  }
  type Element = {
    this_is_element: true;
  };
  interface ElementClass {
    render: () => any;
  }
  interface IntrinsicElements {
    bar: {
      children?: unknown;
    };
  }
  // 全ての要素に対して`key`属性を追加
  interface IntrinsicAttributes {
    key?: string;
  }
}

interface MyProps {
  hoge: string;
}
const MyComponent = ({ hoge }: MyProps) => <bar>{hoge}</bar>;
const elm = <MyComponent key="kkkk" hoge="123" />;

この例では、MyComponentkey属性を渡すことができています。MyComponent自身の定義にkeyはありませんから、これはJSX.IntrinsicAttributesのおかげです。なお、key?: string;というようにkeyを省略可能なものとしている点に注意してください。別に絶対に省略可能である必要はありませんが、これを必須にすると全てのコンポーネントにkeyを渡す必要があり非常に面倒です。

JSX.IntrinsicClassAttributes<T>

追加の属性を定義するもうひとつの方法はJSX.IntrinsicClassAttributes<T>です。その名前が示唆する通り、これはクラスコンポーネント全てに対して適用される属性を表します。関数コンポーネントに対しては適用されません。

これの特徴は、型引数Tを取るようになっている点です。このTは当該のクラスコンポーネント自身のインスタンスの型です。つまり、例えばMyComponentがクラスコンポーネントであるとすると、このコンポーネントは自身が定義する属性に加えてJSX.IntrinsicClassAttributes<MyComponent>で得られる属性も持っていることになります。

あまりうまい例が思いつかないので例は省略しますが、Reactではref属性を定義するのにこれが使われています。

JSX.LibraryManagedAttributes<C, P>

3つ目のJSX.LibraryManagedAttributes<C, P>は、一番新しくて複雑かつ強力な方法です。これは関数コンポーネントとクラスコンポーネントの両方に適用されます。型引数Cはコンポーネント自体の型です。つまり、関数コンポーネントの場合はCは関数の型、クラスコンポーネントの場合はCはクラスの型です(インスタンスの型ではなくクラス自体の型である点に注意してください)。

型引数Pがポイントで、これはそのコンポーネント自身により定義された属性たちの型です。このJSX.LibraryManagedAttributes<C, P>Cに応じてPを加工することができるのです。

それゆえ、このJSX.LibraryManagedAttributes<C, P>が定義されている場合、関数コンポーネント・クラスコンポーネントが取れる属性はJSX.LibraryManagedAttributes<C, P>置き換えられます。追加ではありません。これがこの機能の強力なところです。

Reactではこれを使ってpropTypesdefaultPropsをサポートしています。ここではdefaultPropsを雑に実装してみます。

declare namespace JSX {
  interface ElementChildrenAttribute {
    children: any;
  }
  type Element = {
    this_is_element: true;
  };
  interface ElementClass {
    render: () => any;
  }
  // コンポーネント向けの属性の加工を定義
  type LibraryManagedAttributes<C, P> = C extends { defaultProps: infer D }
    ? Partial<Pick<P, Extract<keyof P, keyof D>>> &
        Pick<P, Exclude<keyof P, keyof D>>
    : P;
}

interface MyProps {
  hoge: string;
  fuga: number;
}
class MyComponent {
  // hoge属性のデフォルト値を定義
  static defaultProps = {
    hoge: "default hoge"
  };
  constructor(props: MyProps) {}
  render() {}
}
// hoge属性が無くてもエラーにならない!
const elm = <MyComponent fuga={1234} />;
// hoge属性があってもOK
const elm2 = <MyComponent hoge="foobar" fuga={1234} />;
// fuga属性が無いのはエラー
const elm3 = <MyComponent hoge="hoge" />;

JSX.LibraryManagedAttributes<C, P>の定義がちょっと目が滑るかもしれませんが、とりあえず後回しにして結果を見ましょう。今回、MyComponentはコンストラクタの引数がMyPropsなので、hogefugaという2つの属性を取るようになっています。どちらも省略可能ではありません。しかし、const elm = ...の例に見えるようにhoge属性を省略してもエラーにはなりません。これは、MyComponentが取る属性がJSX.LibraryManagedAttributes<C, P>によって加工され、hogeが省略可能になったからです。

では、JSX.LibraryManagedAttributes<C, P>は何をしているのでしょうか。ここではconditional typesと、Partial, Pick, Extract, Excludeという組み込み型が使用されています。聞いたこともないという場合はTypeScriptの型入門TypeScriptの型初級を見るとよいでしょう(宣伝)。

ざっくり説明すると、C extends { defaultProps: infer D }というのはCdefaultPropsプロパティを持つかどうかで分岐するという意味です。持っている場合はdefaultPropsの型がDに入ります。持っていない場合はPを加工せずにそのまま返します。Dを取得したあとの部分を見ると、&の前、つまりPartial<Pick<P, Extract<keyof P, keyof D>>>は、PのプロパティのうちDに存在するものは省略可能にするという意味です、&の後、つまりPick<P, Exclude<keyof P, keyof D>>は、PのプロパティのうちDに存在しないものはそのままにするという意味です。

今回のMyComponentに当てはめて考えると、まずPMyProps、つまり{ hoge: string; fuga: number; }です。また、Ctypeof MyComponentです。MyComponentdefaultPropsというstaticプロパティを持つので、これの型がDに入ります。具体的には、D{ hoge: string; }です。PのうちDに存在するもの、つまりhogeは省略可能になり、Dに存在しないもの、つまりfugaはそのままですから、JSX.LibraryManagedAttributes<C, P>の結果は{ hoge?: string; fuga: number; }となります。

以上のように、JSX.LibraryManagedAttributes<C, P>を用いることで柔軟に属性を操作できます。また、実はこれがあればJSX.IntrinsicAttributesJSX.IntrinsicClassAttributes<T>は不要です。後者2つは後方互換性のために残されていますが、もし今から新しいライブラリを作って型定義を用意するとなればJSX.LibraryManagedAttributes<C, P>だけで事足りることでしょう。

JSX名前空間の位置について

これが最後の話題ですが、これまでこの記事全体を通してJSX名前空間を扱ってきました。これは明らかにグローバルに存在する名前空間です。ところがグローバルな空間を汚すのはあまり良くないということで、最近この仕様が変更され、ライブラリの名前空間の下にJSX名前空間を配置できるようになりました。

例えばReactの場合、JSX名前空間の代わりにReact.JSXという名前空間を使用できます。こちらのほうがグローバルを汚さないので推奨されていますが、後方互換性の問題もありますからグローバルのJSX名前空間も引き続きサポートされます。

では、ライブラリの名前空間はどのように決まるのでしょうか。それはTypeScriptのコンパイルオプション--jsxFactoryが関係しています。これはJSX記法を脱糖するときに使う関数を指定するオプションです(型にはあまり関係の無い話なので気になる方は自分で調べてみてください)。デフォルトではこれはReact.createElementというメソッドです。このようにjsxFactoryがメソッドの場合、そのメソッドを有するオブジェクトがライブラリの名前空間として扱われます。

一方、--jsxFactory hのようにメソッドではなく単体の関数名を指定した場合、その関数自体が名前空間と見なされます。この場合はh.JSXという名前空間が参照されます。

では、最後にこれの例を見ましょう。実は--jsxFactory hの代わりにファイル中に/* @jsx h */というコメントを入れてもOKなので今回はそれで対応しています。

/* @jsx h */
declare namespace h.JSX {
  interface ElementChildrenAttribute {
    children: any;
  }
  type Element = {
    this_is_element: true;
  };
  interface ElementClass {
    render: () => any;
  }
  type LibraryManagedAttributes<C, P> = C extends { defaultProps: infer D }
    ? Partial<Pick<P, Extract<keyof P, keyof D>>> &
        Pick<P, Exclude<keyof P, keyof D>>
    : P;
}

interface MyProps {
  hoge: string;
  fuga: number;
}
class MyComponent {
  // hoge属性のデフォルト値を定義
  static defaultProps = {
    hoge: "default hoge"
  };
  constructor(props: MyProps) {}
  render() {}
}
// hoge属性が無くてもエラーにならない!
const elm = <MyComponent fuga={1234} />;
// hoge属性があってもOK
const elm2 = <MyComponent hoge="foobar" fuga={1234} />;

この例では、名前空間をグローバルのJSXではなくh.JSXにしましたが、引き続きちゃんと動作しています。試しにh.JSXfoobar.JSXなど違うものにしてみると、この名前空間は認識されなくなり上のコードはエラーとなります。

このようにJSX名前空間をライブラリの名前空間の下に置くことができる機能は、複数のJSX対応ライブラリを同時に使用したいというなかなか壮絶な要望を受けて対応されたものらしいです。

まとめ

この記事ではTypeScriptに組み込みのJSX名前空間を通してJSXの型チェックを操作する方法を解説しました。TypeScriptにおけるJSXの型チェックは、要素にどのような属性を渡すことができるのかという点に特に焦点が当てられています。要素は組み込み要素、関数コンポーネント、クラスコンポーネントの3種類があり、受け入れられる属性の定義方法はそれぞれで異なっています。また、属性を一括で操作する方法も提供されています。

一応Reactと癒着しすぎずに汎用的な仕組みを提供しようとしているようですが、Reactを使っている方は何となくお察しのとおり、かなりReactに引きずられているように見えます。

ともかく、これでTypeScriptがJSXを使うライブラリ向けにどのようなサポートを提供しているのか分かりましたから、次のステップとしてはReactの型定義、つまり@types/reactを読んでみるのも良いかもしれません。JSX名前空間を取っ掛かりとすることでかなり読解できるはずです。また、もし自分でJSXをサポートするライブラリを作りたくなったらこの記事を思い出して型定義を書いてみましょう。


  1. 今はあまり使われていないので詳細は省きますが、「JSX.ElementClassに当てはまるオブジェクトを返す関数」もクラスと見なされる仕様になっています。後述の関数コンポーネントとかなり紛らわしいですね。 

  2. Reactでは組み込み要素でもkey属性が使用できますが、これは別途定義されておりJSX.IntrinsicAttributesによるものではありません。 

332
247
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
332
247