17
12

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

TypeScript Handbook を読む (18. JSX)

Last updated at Posted at 2017-09-06

TypeScript Handbook を読み進めていく第十八回目。

  1. Basic Types
  2. Variable Declarations
  3. Interfaces
  4. Classes
  5. Functions
  6. Generics
  7. Enums
  8. Type Inference
  9. Type Compatibility
  10. Advanced Types
  11. Symbols
  12. Iterators and Generators
  13. Modules
  14. Namwspaces
  15. Namespaces and Modules
  16. Module Resolution
  17. Declaration Merging
  18. JSX (今ココ)
  19. Decorators
  20. Mixins
  21. Triple-Slash Directives
  22. Type Checking JavaScript Files

JSX

原文

Introduction

JSX は XML に似た、埋め込み構文であり、JavaScript コードに変換することができます。
JSX は React フレームワークによって有名になりましたが、他の実装でも使用されています。
TypeScript では JSX の埋め込み、型チェック、および JavaScript への直接変換をサポートしています。

Basic usage

JSX を使用するには以下のふたつが必要です。

  1. ファイルの拡張子を .tsx にする
  2. jsx オプションを有効にする

TypeScript では perservereactreact-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 ファイルでは角括弧によるキャストを禁止しています。

TypeScript
var foo = <foo>bar;

.tsx ファイルでは上記の構文を使用できないため、代わりに as 演算子を使用してください。
as 演算子を使用すると先ほどの例は以下のように書き換えることができます。

TypeScript
var foo = bar as foo;

as 演算子は .ts ファイルと .tsx ファイルのどちらでも使用することができます。

Type Checking

JSX における型チェックを理解するために、まず固有要素と値に基づく要素の違いを理解しておきましょう。
<expr /> という JSX 文が与えられた場合、expr は環境固有のもの (DOM 環境下における divspan 等) か、自作コンポーネントのいずれかを指すことになります。
これは以下の 2 点において重要です。

  1. React では固有要素は文字列として出力 (React.createElement("div")) されるのに対し、自作コンポーネントはそのまま出力されます (React.createElement(MyComponent))。
  2. JSX 要素に渡された属性はそれぞれ異なる方法で解決されます。
    固有要素の場合、属性は 固有の ものである必要がありますが、自作コンポーネントであれば独自の属性を指定することができます。

TypeScript では React と同じ方法 でこれらの要素を区別します。
つまり、小文字で始まる要素は固有要素として、大文字で始まる要素は値に基づく要素として扱います。

Intrinsic elements

固有要素は JSX.IntrinsicElements という特殊なインタフェースの中から検索されます。
デフォルトでは、このインタフェースが指定されていなければ型チェックは行われません。
このインタフェースが指定されている場合は JSX.IntrinsicElements のプロパティの中から固有要素の名前が検索されます。

TypeScript
declare namespace JSX {
    interface IntrinsicElements {
        foo: any
    }
}

<foo />; // OK
<bar />; // エラー

上記の例では <foo /> は正しく動作しますが、<bar />JSX.IntrinsicElements 内に指定されていないため、正しく動作しません。

以下のように、すべての要素用の文字列インデクサを指定することも可能です

TypeScript
declare namespace JSX {
   interface IntrinsicElements {
       [elemName: string]: any;
   }
}

Value-based elements

値に基づく要素は単純にスコープ内の識別子の中から検索されます。

TypeScript
import MyComponent from "./myComponent";

<MyComponent />; // OK
<SomeOtherComponent />; // エラー

値に基づく要素は以下の 2 通りの方法で定義できます。

  1. Stateless Functional Component (SFC)
  2. Class Component

JSX 構文からは値に基づく要素が上記のどちらであるか判断することができないため、まずはオーバーロードの解決を使って Stateless Functional Component として解決を試みます。
SFC として解決できなかった場合、続いて Class Component として解決を試みます。
それでも解決できなかった場合はエラーとなります。

Stateless Functional Component

名前の表す通り、このコンポーネントは第一引数に props オブジェクトを受け取る JavaScript 関数として定義します。
また、関数の戻り値の型は JSX.Element に代入可能である必要があります。

TypeScript
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 関数であるため、オーバーロードを使用することも可能です。

TypeScript
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 のクラスの場合はそのクラスのインスタンスの型がインスタンス型になりますし、ファクトリ関数の場合はその関数の戻り値の型がインスタンス型になります。

TypeScript
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 で使用できるようにすることが可能です。

TypeScript
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 のプロパティの型が使用されます。

TypeScript
declare namespace JSX {
  interface IntrinsicElements {
    foo: { bar?: boolean }
  }
}

// 'foo' の属性型は '{bar?: boolean}'
<foo bar />;

値に基づく要素の場合、要素のインスタンス型 のプロパティの型に基づいて判定されます。
また、そのプロパティは JSX.ElementAttributesProperty に定義する必要があります。
この時、プロパティは単一のプロパティとして宣言する必要があり、そのプロパティの名前が判定に使用されます。
TypeScript 2.8 からは JSX.ElementAttributesProperty が定義されていない場合には、クラス要素のコンストラクタまたは SFC の第一引数の型が代わりに使用されます。

TypeScript
declare namespace JSX {
  interface ElementAttributesProperty {
    props; // 使用するプロパティ名を指定する
  }
}

class MyComponent {
  // 要素のインスタンス型のプロパティを指定する
  props: {
    foo?: string;
  }
}

// 'MyComponent' の属性型は '{foo?: string}'
<MyComponent foo="bar" />

また、任意のプロパティと必須のプロパティにも対応しています。

TypeScript
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 フレームワークがすべてのタグに必須の属性を持たない限り、これらのインタフェースのプロパティはすべて任意の型とするべきです。

展開演算子も使用できます。

TypeScript
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 は単一のプロパティとして宣言する必要があります。

TypeScript
declare namespace JSX {
  interface ElementChildrenAttribute {
    children: {};  // 使用する children の名前を指定する
  }
}
TypeScript
<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 などのデフォルトの型が上書きされます。

TypeScript
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

波括弧 ({ }) で囲むことで、タグ内に式を埋め込むことが可能です。

TypeScript
var a = <div>
  {["foo", "bar"].map(i => <span>{i / 2}</span>)}
</div>

上記のコードは文字列を数値で割ることができないため、エラーになるでしょう。
pereserve オプションを使用した場合、以下のようなコードが出力されます。

TypeScript
var a = <div>
  {["foo", "bar"].map(function (i) { return <span>{i / 2}</span>; })}
</div>

React integration

JSX を React と一緒に使用する場合、React typeings を使用する必要があります。
この typings では React と一緒に使用するために必要な JSX 名前空間が定義されています。

TypeScript
/// <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 コメントプラグマで指定します。
例えば createElementjsxFactory として指定した場合、<div />React.createElement("div") の代わりに createElement("div") と変換されます。

コメントプラグマを使用する場合は以下のようになります。(TypeScript 2.8):

TypeScript
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` をチェックします。

17
12
0

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
17
12

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?