メインコンテンツへ移動

JSX_TypeScript ノート 17

無料2019-04-27#TypeScript#tsx#TypeScript JSX#tsx props type#TypeScript React#tsx 类型检查

TS も JSX に対して完全な型サポートを提供します

一.基本用法

TypeScript も JSX をサポートしており、Babel と同様に JSX を JavaScript にコンパイルできるだけでなく、型チェックも提供します

TypeScript で JSX を書くには 2 ステップのみ必要です:

  • ソースファイルに .tsx 拡張子を使用

  • --jsx オプションを有効化

さらに、TypeScript は 3 種類の JSX 処理モードを提供し、それぞれ異なるコード生成ルールに対応します:

ModeInputOutputOutput File Extension
preserve<div /><div />.jsx
react<div />React.createElement("div").js
react-native<div /><div />.js

つまり:

  • preserve:.jsx ファイルを生成しますが、JSX 構文を変換せずに保持し、後続のビルド環節(Babel など)に処理を委ねます

  • react:.js ファイルを生成し、JSX 構文を React.createElement に変換します

  • react-native:.js ファイルを生成しますが、JSX 構文を変換せずに保持します

これらのモードは --jsx オプションで指定し、デフォルトは "preserve" で、コード生成のみに影響し、型チェックには影響しません(例えば --jsx "preserve" は変換しないことを要求しますが、それでも JSX に対して型チェックを行います)

具体的な使用では、JSX 構文は完全に一致し、唯一注意が必要なのは型アサーションです

型アサーション

JSX では as type のみ使用できます(山括弧構文が JSX 構文と衝突するため)

let someValue: any = "this is a string";
// <type>
let strLength: number = (<string>someValue).length;

.tsx ファイルではエラーが発生します:

JSX element 'string' has no corresponding closing tag.

'</' expected.

構文の衝突により、<string>someValue 中の型アサーション部分(<string>)が JSX 要素として扱われます。したがって .tsx では as type 形式の型アサーションのみ使用できます:

// as type
let strLength: number = (someValue as string).length;

P.S. TypeScript 型アサーションの詳細情報は、[三。型アサーション](/articles/基本型-typescript ノート 2/#articleHeader4) を参照

二.要素タイプ

JSX 式 <expr /> に対して、expr は環境中の固有要素(intrinsic element、つまり組み込みコンポーネント、例えば DOM 環境中の div または span)か、値ベースの要素(value-based element)、つまりカスタムコンポーネントのいずれかです。2 つの要素の違いは:

  • 生成されるターゲットコードが異なる

    React では、固有要素は文字列を生成します(例えば React.createElement("div"))、カスタムコンポーネントは生成しません(例えば React.createElement(MyComponent)

  • 要素属性(つまり Props)タイプの検索方法が異なる

    固有要素の属性は既知ですが、カスタムコンポーネントは独自の属性セットを指定したい場合があります

形式的には、カスタムコンポーネントは必ず大文字で始まる 必要があり、これで 2 つの JSX 要素を区別します

P.S. 実際には、固有要素/値ベースの要素と組み込みコンポーネント/カスタムコンポーネントは同じことを指し、TypeScript コンパイラにとって、組み込みコンポーネントのタイプは既知で、これを固有要素と呼び、カスタムコンポーネントのタイプはコンポーネント宣言(値)に関連し、これを値ベースの要素と呼びます

固有要素

固有要素のタイプは JSX.IntrinsicElements インターフェースから検索します。このインターフェースを宣言していない場合、すべての固有要素は型チェックされません。宣言している場合、JSX.IntrinsicElements 上で対応する属性を検索し、型チェックの根拠とします:

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

// 正しい
<foo />;
// エラー Property 'bar' does not exist on type 'JSX.IntrinsicElements'.
<bar />;

もちろん、インデックスシグネチャ と組み合わせて、未知の組み込みコンポーネントの使用を許可することもできます:

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

// 正しい
<bar />;

利点は、将来新しい組み込みコンポーネントのサポートを拡張した後、すぐにタイプ宣言を修正する必要がないことで、代償はホワイトリストの厳密な検証を失うことです

値ベースの要素

値ベースの要素は直接スコープ内から対応する識別子を探します。例えば:

import MyComponent from "./myComponent";

// 正しい
<MyComponent />
// エラー Cannot find name 'SomeOtherComponent'.
<SomeOtherComponent />

値ベースの要素は 2 種類あります:

  • ステートレス関数型コンポーネント(Stateless Functional Component、いわゆる SFC)

  • クラスコンポーネント(Class Component)

これら 2 つは JSX 式の形式上からは区別できないため、まず SFC として関数オーバーロードに従って解析を試み、解析に失敗した場合にクラスコンポーネントとして処理し、さらに失敗すればエラーになります

ステートレス関数型コンポーネント

形式的には通常の関数で、最初のパラメータは props オブジェクトであり、戻りタイプは JSX.Element(またはその [サブタイプ](/articles/深入类型系统-typescript 笔记 8/#articleHeader4))である必要があります。例えば:

function Welcome(props: { name: string }) {
  return <h1>Hello, {props.name}</h1>;
}

同様に、[関数オーバーロード](/articles/関数-typescript ノート 5/#articleHeader9) も適用されます:

function Welcome(props: { content: JSX.Element[] | JSX.Element });
function Welcome(props: { name: string });
function Welcome(props: any) {
  <h1>Hello, {props.name}</h1>;
}

<div>
  <Welcome name="Lily" />
  <Welcome content={<span>Hello</span>} />
</div>

P.S. JSX.Element タイプ宣言は [@types/react](https://www.npmjs.com/package/ @types/react) から来ています

クラスコンポーネント

クラスコンポーネントは React.Component を継承し、JavaScript 版とほとんど違いはあ���ません:

class WelcomeClass extends React.Component {
  render() {
    return <h1>Hello, there.</h1>;
  }
}

<WelcomeClass />

Class の [二重タイプ意味](/articles/クラス-typescript ノート 4/#articleHeader8) に類似し、JSX 式 <Expr /> に対して、クラスコンポーネントのタイプは 2 部分に分かれます:

  • 要素クラスタイプ(element class type):Expr のタイプ、つまり typeof WelcomeClass

  • 要素インスタンスタイプ(element instance type):Expr クラスインスタンスのタイプ、つまり { render: () => JSX.Element }

例えば:

// 要素クラスタイプ
let elementClassType: typeof WelcomeClass;
new elementClassType();
// 要素インスタンスタイプ
let elementInstanceType: WelcomeClass;
elementInstanceType.render();

要素インスタンスタイプは JSX.ElementClass のサブタイプである必要があり、デフォルトの JSX.ElementClass タイプは {} で、React では render メソッドを持つ必要があります:

namespace JSX {
  interface ElementClass extends React.Component<any> {
    render(): React.ReactNode;
  }
}

DefinitelyTyped/types/react/index.d.ts から抜粋)

否则エラーになります:

class NotAValidComponent {}
function NotAValidFactoryFunction() {
  return {};
}

<div>
  {/* エラー JSX element type 'NotAValidComponent' is not a constructor function for JSX elements. */}
  <NotAValidComponent />
  {/* エラー JSX element type '{}' is not a constructor function for JSX elements. */}
  <NotAValidFactoryFunction />
</div>

三.属性タイプ

属性チェックはまず要素属性タイプ(element attributes type)を確定する必要があり、固有要素と値ベースの要素は属性タイプに少し違いがあります:

  • 固有要素の属性タイプ:JSX.IntrinsicElements 上対応する属性のタイプ

  • 値ベースの要素属性タイプ:要素インスタンスタイプ上特定属性タイプ上対応する属性のタイプ、この特定属性は JSX.ElementAttributesProperty で指定

P.S. JSX.ElementAttributesProperty を宣言していない場合、コンポーネントクラスコンストラクタまたは SFC 最初のパラメータのタイプを取得します

具体的には、固有要素属性は ahref を例に:

namespace JSX {
  interface IntrinsicElements {
    // 各固有要素及其びその属性タイプを宣言
    a: {
      download?: any;
      href?: string;
      hrefLang?: string;
      media?: string;
      rel?: string;
      target?: string;
      type?: string;
      referrerPolicy?: string;
    }
  }
}

// 要素属性タイプは { href?: string }
<a href="">リンク</a>

値ベースの要素属性は例えば:

namespace JSX {
  // 特定属性名を props に指定
  interface ElementAttributesProperty { props: {}; }
}

class MyComponent extends React.Component {
  // 属性タイプを宣言
  props: {
    foo?: string;
  }
}
// 要素属性タイプは { foo?: string }
<MyComponent foo="bar" />

オプション属性、展開演算子なども同様に適用され、例えば:

class MyComponent extends React.Component {
  // 属性タイプを宣言
  props: {
    requiredProp: string;
    optionalProp?: string;
  }
}

const props = { optionalProp: 'optional' };
// 正しい
<MyComponent { ...props } requiredProp="required" />

P.S. さらに、JSX フレームワークは JSX.IntrinsicAttributes を通じてフレームワークに必要な追加属性を指定できます。例えば React 内の key。詳細は Attribute type checking を参照

P.S. 特殊的には、属性チェックは属性名が合法な JavaScript 識別子の属性に対してのみ行われdata-* などはチェックされません

子コンポーネントタイプチェック

子コンポーネントのタイプは要素属性タイプ上の children 属性から来て、ElementAttributesPropertyprops を指定するのと同様に、ここでは JSX.ElementChildrenAttribute を使用して children を指定します:

namespace JSX {
  // 特定属性名を children に指定
  interface ElementChildrenAttribute { children: {}; }
}

const Wrapper = (props) => (
  <div>
    {props.children}
  </div>
);
<Wrapper>
  <div>Hello World</div>
  {"This is just a JS expression..." + 1000}
</Wrapper>

children にタイプを指定する方法は通常の属性と類似しています:

interface PropsType {
  children: JSX.Element
  name: string
}
class Component extends React.Component<PropsType, {}> {
  render() {
    return (
      <h2>
        {this.props.children}
      </h2>
    )
  }
}

<Component name="hello">
  <h1>Hello World</h1>
</Component>

子コンポーネントタイプが一致しないとエラーになります:

// エラー Type '{ children: Element[]; name: string; }' is not assignable to type 'Readonly<PropsType>'.
<Component name="hello">
  <h1>Hello World</h1>
  <h1>Hi</h1>
</Component>

四.結果タイプ

デフォルトでは、JSX 式の結果タイプは any です:

// a のタイプは any
let a = <a href="" />;
a = {};

JSX.Element を通じて指定できます。例えば React では:

let a = <a href="" />;
// エラー Type '{}' is missing the following properties from type 'Element': type, props, key.
a = {};

対応するタイプ宣言は類似しています:

namespace JSX {
  interface Element<T, P> {
    type: T;
    props: P;
    key: string | number | null;
  }
}

P.S. React 内の具体的な JSX 要素タイプ宣言は DefinitelyTyped/types/react/index.d.ts を参照

五.埋め込まれた式

JSX はタグ内で花括弧構文({ })を通じて式を挿入することを許可します:

const name = 'Josh Perez';
const element = <h1>Hello, {name}</h1>;

Embedding Expressions in JSX から抜粋)

TypeScript もサポートし、埋め込まれた式に対して型チェックを行えます:

const a = <div>
  {/* エラー The left-hand side of an arithmetic operation must be of type 'any', 'number', 'bigint' or an enum type. */}
  {["foo", "bar"].map(i => <span>{i / 2}</span>)}
</div>

六.React との結合

[React タイプ定義](https://www.npmjs.com/package/ @types/react) を導入した後、Props のタイプを簡単に記述できます:

interface WelcomeProps {
  name: string;
}
// Props のタイプを最初のタイプパラメータとして渡す
class WelcomeClass extends React.Component<WelcomeProps, {}> {
  render() {
    return <h1>Hello, {this.props.name}</h1>;
  }
}
// エラー Property 'name' is missing in type '{}' but required in type 'Readonly<WelcomeProps>'.
let errorCase = <WelcomeClass />;
let correctCase = <WelcomeClass name="Lily" />;

P.S. タイプパラメータ及びジェネリックの詳細情報は、[二。タイプ変数](/articles/ジェネリック-typescript ノート 6/#articleHeader3) を参照

ファクトリ関数

React モード(--jsx react)では、具体的に使用する JSX 要素ファクトリメソッドを構成でき、2 つの方法があります:

  • --jsxFactory オプション:プロジェクトレベル構成

  • インライン @jsx コメント指令:ファイルレベル構成

デフォルトは --jsxFactory "React.createElement" で、JSX タグをファクトリメソッド呼び出しに変換します:

const div = <div />;
// コンパイル結果
var div = React.createElement("div", null);

Preact 内では対応する JSX 要素ファクトリメソッドは h です:

/* @jsx preact.h */
import * as preact from "preact";
<div />;
// または
/* @jsx h */
import { h } from "preact";
<div />;

P.S. 注意、 @jsx コメント指令は必ずファイルの先頭行に出現する必要があり、その他の位置では無効です

コンパイル結果はそれぞれ:

/* @jsx preact.h */
var preact = require("preact");
preact.h("div", null);
// または
/* @jsx h */
var preact_1 = require("preact");
preact_1.h("div", null);

P.S. さらに、ファクトリメソッド構成は JSX 名前空間の検索にも影響します。例えばデフォルト --jsxFactory React.createElement の場合、優先的に React.JSX を検索し、次にグローバル JSX 名前空間を見ます。--jsxFactory h を指定すると、優先的に h.JSX を検索します

七.まとめ

TypeScript 中 JSX のタイプサポートは要素タイプ、属性タイプ、結果タイプの 3 部分に分かれ、下図の通り:

tsx

参考資料

コメント

コメントはまだありません

コメントを書く