TypeScript Handbook を読み進めていく第十八回目。
- Basic Types
- Variable Declarations
- Interfaces
- Classes
- Functions
- Generics
- Enums
- Type Inference
- Type Compatibility
- Advanced Types
- Symbols
- Iterators and Generators
- Modules
- Namwspaces
- Namespaces and Modules
- Module Resolution
- Declaration Merging
- JSX (今ココ)
- Decorators
- Mixins
- Triple-Slash Directives
- Type Checking JavaScript Files
JSX
Introduction
JSX は XML に似た、埋め込み構文であり、JavaScript コードに変換することができます。
JSX は React フレームワークによって有名になりましたが、他の実装でも使用されています。
TypeScript では JSX の埋め込み、型チェック、および JavaScript への直接変換をサポートしています。
Basic usage
JSX を使用するには以下のふたつが必要です。
- ファイルの拡張子を
.tsx
にする -
jsx
オプションを有効にする
TypeScript では perserve
、react
、react-native
の 3 種類の JSX モードを提供しています。
preserve
モードでは他の変換フェーズ (Babel 等) で処理するために JSX をそのまま出力します。
この時の出力ファイルの拡張子は .jsx
になります。
react
モードでは React.createElement
を出力するため、他のツールで JSX を変換する必要がありません。
また、出力ファイルの拡張子は .js
になります。
react-native
モードは preseve
モードと同じく、JSX をそのまま出力しますが、拡張子は .js
になります。
Mode | Input | Output |
---|---|---|
preserve | <div /> | <div /> |
react | <div /> | React.creteElement("div") |
react-native | <div /> | <div /> |
JSX モードは --jsx
コマンドラインオプションまたは tsconfig.json で指定します。
React
という識別子はハードコーディングされているため、大文字の R で始まる識別子 (React) が使用可能でなければなりません。
The as
operator
TypeScript では以下のように角括弧を使用してキャストすることができますが、JSX の構文と区別することが困難であるため、.tsx
ファイルでは角括弧によるキャストを禁止しています。
var foo = <foo>bar;
.tsx
ファイルでは上記の構文を使用できないため、代わりに as
演算子を使用してください。
as
演算子を使用すると先ほどの例は以下のように書き換えることができます。
var foo = bar as foo;
as
演算子は .ts
ファイルと .tsx
ファイルのどちらでも使用することができます。
Type Checking
JSX における型チェックを理解するために、まず固有要素と値に基づく要素の違いを理解しておきましょう。
<expr />
という JSX 文が与えられた場合、expr
は環境固有のもの (DOM 環境下における div
や span
等) か、自作コンポーネントのいずれかを指すことになります。
これは以下の 2 点において重要です。
- React では固有要素は文字列として出力 (
React.createElement("div")
) されるのに対し、自作コンポーネントはそのまま出力されます (React.createElement(MyComponent))。 - JSX 要素に渡された属性はそれぞれ異なる方法で解決されます。
固有要素の場合、属性は 固有の ものである必要がありますが、自作コンポーネントであれば独自の属性を指定することができます。
TypeScript では React と同じ方法 でこれらの要素を区別します。
つまり、小文字で始まる要素は固有要素として、大文字で始まる要素は値に基づく要素として扱います。
Intrinsic elements
固有要素は JSX.IntrinsicElements
という特殊なインタフェースの中から検索されます。
デフォルトでは、このインタフェースが指定されていなければ型チェックは行われません。
このインタフェースが指定されている場合は JSX.IntrinsicElements
のプロパティの中から固有要素の名前が検索されます。
declare namespace JSX {
interface IntrinsicElements {
foo: any
}
}
<foo />; // OK
<bar />; // エラー
上記の例では <foo />
は正しく動作しますが、<bar />
は JSX.IntrinsicElements
内に指定されていないため、正しく動作しません。
以下のように、すべての要素用の文字列インデクサを指定することも可能です
declare namespace JSX {
interface IntrinsicElements {
[elemName: string]: any;
}
}
Value-based elements
値に基づく要素は単純にスコープ内の識別子の中から検索されます。
import MyComponent from "./myComponent";
<MyComponent />; // OK
<SomeOtherComponent />; // エラー
値に基づく要素は以下の 2 通りの方法で定義できます。
- Stateless Functional Component (SFC)
- Class Component
JSX 構文からは値に基づく要素が上記のどちらであるか判断することができないため、まずはオーバーロードの解決を使って Stateless Functional Component として解決を試みます。
SFC として解決できなかった場合、続いて Class Component として解決を試みます。
それでも解決できなかった場合はエラーとなります。
Stateless Functional Component
名前の表す通り、このコンポーネントは第一引数に props
オブジェクトを受け取る JavaScript 関数として定義します。
また、関数の戻り値の型は JSX.Element
に代入可能である必要があります。
interface FooProp {
name: string;
X: number;
Y: number;
}
declare function AnotherComponent(prop: {name: string});
function ComponentFoo(prop: FooProp) {
return <AnotherComponent name={prop.name} />;
}
const Button = (prop: {value: string}, context: { color: string }) => <button>
SFC は単なる JavaScript 関数であるため、オーバーロードを使用することも可能です。
interface ClickableProps {
children: JSX.Element[] | JSX.Element
}
interface HomeProps extends ClickableProps {
home: JSX.Element;
}
interface SideProps extends ClickableProps {
side: JSX.Element | string;
}
function MainButton(prop: HomeProps): JSX.Element;
function MainButton(prop: SideProps): JSX.Element {
...
}
Class Component
Class Component の型を定義することは可能ですが、そのためには 要素のクラス型 と 要素のインスタンス型 について理解する必要があります。
<Expr />
が与えられた場合、Expr
の型が 要素のクラス型 に該当します。
上記の例で言うと、MyComponent
が ES6 のクラスの場合には要素のクラス型はそのクラスのコンストラクタと静的メンバになりますし、MyComponent
がファクトリ関数の場合には関数になります。
クラス型が決まると、そのクラス型のコンストラクタと (存在するなら) 関数シグネチャの戻り値の共用体がインスタンス型になります。
先ほどの例で言うと、クラス型が ES6 のクラスの場合はそのクラスのインスタンスの型がインスタンス型になりますし、ファクトリ関数の場合はその関数の戻り値の型がインスタンス型になります。
class MyComponent {
render() {}
}
// コンストラクタシグネチャを使用する
var myComponent = new MyComponent();
// 要素のクラス型 => MyComponent
// 要素のインスタンス型 => { render: () => void }
function MyFactoryFunction() {
return {
render: () => {
}
}
}
// 呼び出しシグネチャを使用する
var myComponent = MyFactoryFunction();
// 要素のクラス型 => FactoryFunction
// 要素のインスタンス型 => { render: () => void }
要素のインスタンス型は JSX.ElementClass
に代入可能である必要があり、そうでない場合はエラーとなります。
デフォルトでは JSX.ElementClass
は {}
ですが、このインタフェースを拡張することで、特定のインタフェースを持つ型だけを JSX で使用できるようにすることが可能です。
declare namespace JSX {
interface ElementClass {
render: any;
}
}
class MyComponent {
render() {}
}
function MyFactoryFunction() {
return { render: () => {} }
}
<MyComponent />; // OK
<MyFactoryFunction />; // OK
class NotAValidComponent {}
function NotAValidFactoryFunction() {
return {};
}
<NotAValidComponent />; // エラー
<NotAValidFactoryFunction />; // エラー
上記の例では
render
関数を持つインタフェースだけに制限している
Attribute type checking
属性の型チェックを行うためには、まず 要素の属性の型 を判定する必要があります。
この判定方法は固有要素と値に基づく要素とで若干異なります。
固有要素の場合、JSX.IntrinsicElements
のプロパティの型が使用されます。
declare namespace JSX {
interface IntrinsicElements {
foo: { bar?: boolean }
}
}
// 'foo' の属性型は '{bar?: boolean}'
<foo bar />;
値に基づく要素の場合、要素のインスタンス型 のプロパティの型に基づいて判定されます。
また、そのプロパティは JSX.ElementAttributesProperty
に定義する必要があります。
この時、プロパティは単一のプロパティとして宣言する必要があり、そのプロパティの名前が判定に使用されます。
TypeScript 2.8 からは JSX.ElementAttributesProperty
が定義されていない場合には、クラス要素のコンストラクタまたは SFC の第一引数の型が代わりに使用されます。
declare namespace JSX {
interface ElementAttributesProperty {
props; // 使用するプロパティ名を指定する
}
}
class MyComponent {
// 要素のインスタンス型のプロパティを指定する
props: {
foo?: string;
}
}
// 'MyComponent' の属性型は '{foo?: string}'
<MyComponent foo="bar" />
また、任意のプロパティと必須のプロパティにも対応しています。
declare namespace JSX {
interface IntrinsicElements {
foo: { requiredProp: string; optionalProp?: number }
}
}
<foo requiredProp="bar" />; // OK
<foo requiredProp="bar" optionalProp={0} />; // OK
<foo />; // エラー、requiredProp が足りない
<foo requiredProp={0} />; // エラー、requiredProp は文字列型でないといけない
<foo requiredProp="bar" unknownProp />; // エラー、unknownProp は存在しない
<foo requiredProp="bar" some-unknown-prop />; // OK、なぜなら 'some-unknown-prop' は正しい識別子ではないため。
属性名が正しい JavaScript の識別子 (data-*
属性等) でない場合、該当するプロパティが存在しなくてもエラーにはなりません。
JavaScript では
-
を含む識別子は不正だから無視されている、と。
加えて、JSX.IntrinsicAttributes
インタフェースはコンポーネントのプロパティや引数を使用しない JSX フレームワークが使う、追加のプロパティ (React の key
など) を指定するためにも使用できます。
さらに、JSX.IntrinsicClassAttributes<T>
クラスは (SFCではない) クラスコンポーネントの追加の属性を指定するために使用できます。
この場合、ジェネリック引数はクラスのインスタンス型と対応することになり、React では Ref<T>
型の ref
属性で使用されています。
一般的に、使用している JSX フレームワークがすべてのタグに必須の属性を持たない限り、これらのインタフェースのプロパティはすべて任意の型とするべきです。
展開演算子も使用できます。
var props = { requiredProp: "bar" };
<foo {...props} />; // OK
var badProps = {};
<foo {...badProps} />; // エラー
Children Type Checking
TypeScript 2.3 から 子要素 (children) の型チェックができるようになりました。
children は 要素の属性の型 の特殊なプロパティです。
props の名前を判断するために JSX.ElementAttributesProperty
を使用するのと同じように、children の名前を判断するために JSX.ElementChildrenAttribute
を使用します。
JSX.ElementChildrenAttribute
は単一のプロパティとして宣言する必要があります。
declare namespace JSX {
interface ElementChildrenAttribute {
children: {}; // 使用する children の名前を指定する
}
}
<div>
<h1>Hello</h1>
</div>;
<div>
<h1>Hello</h1>
World
</div>;
const CustomComp = (props) => <div>props.children</div>
<CustomComp>
<div>Hello World</div>
{"This is just a JS expression..." + 1000}
</CustomComp>
他の属性のように children の型を指定することも可能です。
その場合、React typings などのデフォルトの型が上書きされます。
interface PropsType {
children: JSX.Element
name: string
}
class Component extends React.Component<PropsType, {}> {
render() {
return (
<h2>
{this.props.children}
</h2>
)
}
}
// OK
<Component>
<h1>Hello World</h1>
</Component>
// エラー children は JSX.Element 型であり、JSX.Element の配列ではない
<Component>
<h1>Hello World</h1>
<h2>Hello World</h2>
</Component>
// エラー: children は JSX.Element 型であり、JSX.Element または文字列の配列ではない
<Component>
<h1>Hello</h1>
World
</Component>
The JSX result type
デフォルトでは JSX 式の戻り値は any
型です。
JSX.Element
インタフェースを指定することでこれを変更することが可能ですが、要素、属性、子要素の型情報はブラックボックス化されており、このインタフェースから取得することはできません。
Embedding Expressions
波括弧 ({ }
) で囲むことで、タグ内に式を埋め込むことが可能です。
var a = <div>
{["foo", "bar"].map(i => <span>{i / 2}</span>)}
</div>
上記のコードは文字列を数値で割ることができないため、エラーになるでしょう。
pereserve
オプションを使用した場合、以下のようなコードが出力されます。
var a = <div>
{["foo", "bar"].map(function (i) { return <span>{i / 2}</span>; })}
</div>
React integration
JSX を React と一緒に使用する場合、React typeings を使用する必要があります。
この typings では React と一緒に使用するために必要な JSX
名前空間が定義されています。
/// <reference path="react.d.ts" />
interface Props {
foo: string;
}
class MyComponent extends React.Component<Props, {}> {
render() {
return <span>{this.props.foo}</span>
}
}
<MyComponent foo="bar" />; // OK
<MyComponent foo={0} />; // エラー
Factory Functions
jsx: react
コンパイラが指定された場合、正確なファクトリ関数が使用されます。
このファクトリ関数は jsxFactory
コマンドライン引数か、ファイルごとの @jsx
コメントプラグマで指定します。
例えば createElement
を jsxFactory
として指定した場合、<div />
は React.createElement("div")
の代わりに createElement("div")
と変換されます。
コメントプラグマを使用する場合は以下のようになります。(TypeScript 2.8):
import preact = require("preact");
/* @jsx preact.h */
const x = <div />;
これは以下のように出力されます。
js:JavaScript
const preact = require("preact");
const x = preact.h("div", null);
ファクトリ関数の指定は (型チェックを行うための) `JSX` 名前空間の検索にも影響します。
ファクトリ関数として `React.createElement` (デフォルト) が指定されている場合、コンパイラはグローバル `JSX` の前に `React.JSX` をチェックしますし、`h` が指定されている場合は `h.JSX` をチェックします。