.tsxファイルについてドキュメントを読む(その2)

前回に引き続き、TypeScriptドキュメントのJSX章を読んで得た知見をまとめます。なお、ここに出しているコード例はドキュメントにあったものをほぼ踏襲し、適宜コメント等を書き換えたりしています。

コンポーネント

Reactにおいて、組み込み要素でないもの(Value-based elements)には、関数コンポーネントとクラスコンポーネントという2通りの定義のやり方があります。

コンポーネントにより UI を独立した再利用できる部品に分割し、部品それぞれを分離して考えることができるようになります。

コンポーネントと props – React

TypeScriptとしては、式を関数コンポーネント→クラスコンポーネントの順に解決しようと試みます。それに失敗した場合はエラーとなります。

関数コンポーネント

名前の通り、関数コンポーネントは第一引数にpropsオブジェクトをとる関数として定義されます。TypeScriptでは、この関数コンポーネントの返り値の型がJSX.Elementsに割り当て可能であることが必須です。

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 />
);

また、関数コンポーネントは単なる関数であるので、通常の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;
function MainButton(prop: ClickableProps): JSX.Element {
  // ...
}

ちなみに、以前はStateless Function Components(SFC)と呼ばれていましたが、最近のReactにおいて関数コンポーネントはステートレスとは見なされなくなったため、SFC型とそのエイリアスStatelessComponentsは非推奨になっています。

クラスコンポーネント

ここではクラスコンポーネントの紹介をするにあたり、「要素クラス型(the element class type)」と「要素インスタンス型(the element instance type)」という言葉を使います。

<Expr />が指定された場合、要素クラスの型はExpr型になります。 上記の例では、MyComponentがES6のクラスの場合、そのクラスの型はクラスになります。 もし、MyComponentがファクトリー関数であった場合、そのクラス型は関数になります。 クラスの型が確立されると、インスタンス型はクラス型の呼び出しシグネチャの戻り値の型とコンストラクタのシグネチャの和集合によって決定されます。 したがって、ES6クラスの場合はインスタンスの型はそのクラスのインスタンスの型になり、 ファクトリ関数の場合は関数から返される値の型になります。
https://js.studio-kingdom.com/typescript/handbook/jsx

↑よくわかりませんでした

属性の型チェック

ここで言う属性とは、HTMLにおける「属性(attribute)」のことです。

TypeScriptでは、この属性の型チェックも行います。

declare namespace JSX {
  interface IntrinsicElements {
    foo: { bar?: boolean };
  }
}
// element attributes type for 'foo' is '{bar?: boolean}'
<foo bar />;

ユーザが定義するような値ベースの要素(value-based elements)では、もう少し複雑になります。属性の型は、先に決定された「要素インスタンス型」にあるプロパティの型によって決まります。

declare namespace JSX {
  interface ElementAttributesProperty {
    props; // specify the property name to use
  }
}
class MyComponent {
  // specify the property on the element instance type
  props: {
    foo?: string;
  };
}
// element attributes type for 'MyComponent' is '{foo?: string}'
<MyComponent foo="bar" />;

TypeScript 2.8では、上のコード例にあるようなJSX.ElementAttributesPropertyが提供されない場合、クラス要素のコンストラクタや関数コンポーネントの呼び出しの第一引数の型が代わりに使用されます。

要素の属性型は、JSXの属性を型チェックするために使用されるもので、必須のプロパティと任意のプロパティとが用意されています。なお、属性名がdata-*のように有効なJS識別子でない場合には、エラーとみなされないようなので注意が必要です。

declare namespace JSX {
  interface IntrinsicElements {
    foo: { requiredProp: string; optionalProp?: number };
  }
}

<foo requiredProp="bar" />; 
// ok

<foo requiredProp="bar" optionalProp={0} />; 
// ok

<foo />; 
// error, requiredProp is missing

<foo requiredProp={0} />; 
// error, requiredProp should be a string

<foo requiredProp="bar" unknownProp />; 
// error, unknownProp does not exist

<foo requiredProp="bar" some-unknown-prop />; 
// ok, because 'some-unknown-prop' is not a valid identifier

また、JSX.IntrinsicAttributesインターフェースを使用すると、コンポーネントのpropや引数では一般的に使用されない、JSXフレームワークが使用する追加のプロパティ(例えば、Reactのkey)を指定することができます。さらに言うと、一般的なJSX.IntrinsicClassAttributes<T>型も、クラスコンポーネント(関数コンポーネントではない)に対してのみ、同じ種類の追加属性を指定するために使用されるかもしれません。この型では、ジェネリックパラメータがクラスインスタンス型に対応します。React では、これは Ref<T> 型の ref 属性を許可するために使用されます。一般的に言って、JSXフレームワークのユーザーがすべてのタグで何らかの属性を提供する必要がある場合を除き、これらのインターフェースのプロパティはすべてオプションであるべきです。

また、spread演算子も有効です。

const props = { requiredProp: "bar" };
<foo {...props} />; // ok

const badProps = {};
<foo {...badProps} />; // error

参考