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, foo
とbar
を受け取るコンポーネントとして型を定義しています。下半分でMyComponent
をコンポーネントとしてJSXから使用しています。このようにHTMLの属性のような記法でpropsをコンポーネントに渡すことができます。このコードではfoo
もbar
も文字列を渡しており、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
の型が違う)ので型エラーとなります。
このことが分かれば、要素の宣言において省略可能な子や省略可能な属性を宣言することができます。次の例ではfoo
やbar
の属性や子要素の型をいじってみました。
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におけるユーザー定義コンポーネントの例です。
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" />;
この例では、MyComponent
にkey
属性を渡すことができています。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ではこれを使ってpropTypes
とdefaultProps
をサポートしています。ここでは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
なので、hoge
とfuga
という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 }
というのはC
がdefaultProps
プロパティを持つかどうかで分岐するという意味です。持っている場合はdefaultProps
の型がD
に入ります。持っていない場合はP
を加工せずにそのまま返します。D
を取得したあとの部分を見ると、&
の前、つまりPartial<Pick<P, Extract<keyof P, keyof D>>>
は、P
のプロパティのうちD
に存在するものは省略可能にするという意味です、&
の後、つまりPick<P, Exclude<keyof P, keyof D>>
は、P
のプロパティのうちD
に存在しないものはそのままにするという意味です。
今回のMyComponent
に当てはめて考えると、まずP
はMyProps
、つまり{ hoge: string; fuga: number; }
です。また、C
はtypeof MyComponent
です。MyComponent
はdefaultProps
というstaticプロパティを持つので、これの型がD
に入ります。具体的には、D
は{ hoge: string; }
です。P
のうちD
に存在するもの、つまりhoge
は省略可能になり、D
に存在しないもの、つまりfuga
はそのままですから、JSX.LibraryManagedAttributes<C, P>
の結果は{ hoge?: string; fuga: number; }
となります。
以上のように、JSX.LibraryManagedAttributes<C, P>
を用いることで柔軟に属性を操作できます。また、実はこれがあればJSX.IntrinsicAttributes
やJSX.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.JSX
をfoobar.JSX
など違うものにしてみると、この名前空間は認識されなくなり上のコードはエラーとなります。
このようにJSX
名前空間をライブラリの名前空間の下に置くことができる機能は、複数のJSX対応ライブラリを同時に使用したいというなかなか壮絶な要望を受けて対応されたものらしいです。
まとめ
この記事ではTypeScriptに組み込みのJSX
名前空間を通してJSXの型チェックを操作する方法を解説しました。TypeScriptにおけるJSXの型チェックは、要素にどのような属性を渡すことができるのかという点に特に焦点が当てられています。要素は組み込み要素、関数コンポーネント、クラスコンポーネントの3種類があり、受け入れられる属性の定義方法はそれぞれで異なっています。また、属性を一括で操作する方法も提供されています。
一応Reactと癒着しすぎずに汎用的な仕組みを提供しようとしているようですが、Reactを使っている方は何となくお察しのとおり、かなりReactに引きずられているように見えます。
ともかく、これでTypeScriptがJSXを使うライブラリ向けにどのようなサポートを提供しているのか分かりましたから、次のステップとしてはReactの型定義、つまり@types/react
を読んでみるのも良いかもしれません。JSX
名前空間を取っ掛かりとすることでかなり読解できるはずです。また、もし自分でJSXをサポートするライブラリを作りたくなったらこの記事を思い出して型定義を書いてみましょう。