ReactをTypeScriptで書く3: React編

メモです。
そして前回からの続きです。

追記: 2019/07/16
全体的に項目を修正しました。

Function Component

Function Componentの基本形はReact.FC型を使用する。
ジェネリクスにpropsの型を指定することができる。
PropTypesなどで指定していたpropsの型指定が、型エイリアスに移っただけ。

src/components/content.tsx
import * as React from 'react';

// Introのpropsのtype aliasを定義
type ContentProps = {
  title: string;
}

// React.FC型を用いる
// ジェネリクスにIntroPropsを指定
const Content: React.FC<ContentProps> = props => {
  return (
    <>
      <h2>{props.title}</h2>
      <p className="App-intro">
        {props.children}
      </p>
    </>
  )
}

export default Content;

下記はpropsとして必須な"title"がないためエラーになる。

src/components/parent.tsx
import Content from './content';
/* 〜〜〜省略〜〜〜 */
<Content />

React.FC型

v16.8からReact Hooksが登場し、Stateless Functional ComponentはStatelessでなくなるため、Function Componentに名称が変わった。
それに伴い、React.FunctionComponent型が登場し、React.FC型は短縮系。

interfaceか、型エイリアスか

propsの型指定は、interfaceよりも型エイリアスが使われる。
次の記事では、Reactのprops、stateに型エイリアスを使うことが推奨されている。

Interface vs Type alias in TypeScript 2.7 – Martin Hochel – Medium

  • 記述量が短い
  • 一貫性のある記述(※interfaceでextendsしなくていいといった感じ)
  • 拡張の必要がない

というような理由。
interfaceで書くこと自体が非推奨という内容ではない。

useStateの型

v16.8から登場したReact.useState()は下記の通り。
初期値で型推論が行われる。
ジェネリクスで型を指定することも可能。

src/components/content.tsx
type StateType = "fetching" | "success" | "error";

const Component = (props: ComponentProps) => {
  // boolean
  const [fetching, setFetching] = React.useState(true);

  // "fetching" | "success" | "error"
  const [state, setState] = React.useState<StateType>('fetching');

  React.useEffect(() => {
    setFetching("done"); // エラー
    setFetching(false); // OK
    setState(true); // エラー
    setState("successs"); // typoなのでエラー
    setState("success"); // OK
  }, []);

  return <div>{!fetching && state === "success" && "OK"}</div>;
};

Class Component

Class Componentではこうなる。

src/components/Header.tsx
import * as React from 'react';
import logo from '../logo.svg';

// Headerのpropsのtype aliasを定義
type HeaderProps = {
  text?: string;
}

// HeaderのLocalStateのtype aliasを定義
type HeaderState = {
  rotationSpeed: number;
}

// (1) React.Component型を用いる
// ジェネリクスの第1引数はprops、第2引数はLocalState
class Header extends React.Component<HeaderProps, HeaderState> {

  // (2) defaultPropsのstaticメソッド
  static defaultProps: HeaderProps = {
    text: 'Welcome to React',
  };

  constructor(props: HeaderProps) {
    super(props);
    this.state = {
      rotationSpeed: 100,
    }
    this.changeRotationSpeed = this.changeRotationSpeed.bind(this);
  }

  changeRotationSpeed = () => {
    if (this.state.rotationSpeed > 1) {
      this.setState({
        rotationSpeed: this.state.rotationSpeed - 1
      })
    }
  }

  // (3) public アクセス修飾子
  public render() {
    return (
      <header className="App-header">
        <img
          src={logo}
          className="App-logo"
          alt="logo"
          onClick={this.changeRotationSpeed}
          style={{
            animationDuration: `${this.state.rotationSpeed}s`
          }} />
        <h1 className="App-title">{this.props.text}</h1>
      </header>
    )
  }

}

export default Header;

(1)ではReact.Componentをextendsして、そこにジェネリクスを指定している。

src/components/Header.tsx
class Header extends React.Component<HeaderProps, HeaderState> {
 /* 略 */
}

(2)では、defaultPropsをstaticメソッド(後述)にして、propsの初期値を設定している。

src/components/Header.tsx
static defaultProps: HeaderProps = {
  text: 'Welcome to React',
};

(3)では、renderをpublicメソッド(後述)として定義する。

src/components/Header.tsx
public render() {
 /* 略 */
}

新しく見る記述については以下の通り。

Object型

"React.Component"の型引数は次のようになっている。

node_modules/@types/react/index.d.ts
<P = {}, S = {}, SS = any>

第1引数(props)と第2引数(state)の初期型は{}になっている。
これは見た目通り空のオブジェクトを表し、Object型という。

基本的にオブジェクトが入るようになっている。
でも、JavaScriptはプリミティブ値がオブジェクトのように振る舞える仕組みがある。
ラッパーオブジェクト · JavaScriptの入門書 #jsprimer

そのような仕組みがあるためか、ラッパーオブジェクトを持たないnullやundefinedを除くと、なんでも入ってしまう。
本当にオブジェクトを入れたい場合、Object型ではなくobject型を使う。
(objectの中の型は……。)

// HogeがObject型
type Hoge = {};
let Foo: Hoge = 'bar'; // これはOK

// Fugaがobject型
type Fuga = object;
let Bar: Fuga = 'bar'; // これはエラー
const Baz: Fuga = {
  piyo: 'あああああ', // これはOK
};

アクセス修飾子

render関数の前にある"public"は、アクセス修飾子の一つ。
アクセス修飾子は、classの外からのメンバへのアクセスを防ぐために使われる。
修飾子の種類によって、アクセシビリティ(アクセス可能な範囲)のレベルが決まる。

publicは、どこからでもアクセスできる。
privateとすることで、Class外からアクセスできなくなる。
protectedでは、他Classからアクセスできなくなる(サブクラスはOK)。
staticは、インスタンスなしに直接呼び出せる。

tslintを導入していると、"member-access: no-public"という設定になっていて、アクセス修飾子を明示しないとエラーになる。
"member-access"をfalseにすることで、アクセス修飾子の付与を強制されない。
falseにされていることが多い。

async/await

上記のClass Componentに、ロード完了したら読み込むコンポーネントを追加してみる。
0.05秒ごとにReactロゴのanimation-durationを1秒減らし、
animation-durationが1sになったらロード終了とする。

HeaderState型に「isLoading」をboolean型で追加。

src/components/Header.tsx
type HeaderState = {
  rotationSpeed: number;
  isLoading: boolean;};

次に、tick関数というのを作って、繰り返し0.05秒待つ処理を作る。
TypeScriptでは、標準でasync/awaitが使える。
戻り値の型はPromise、中で返す値はジェネリクスで指定する。
今回は返す値がないので、voidにしている。

src/components/Header.tsx
// tick関数の戻り値をPromise<型>で指定
tick = async (): Promise<void> => {
  this.changeRotationSpeed();
  await new Promise(resolve => setTimeout(resolve, 50));
  if (this.state.rotationSpeed > 1) {
    this.tick();
  } else {
    this.setState({
      isLoading: false,
    });
  }
};

refの型指定(createRef)

おまけに、refの型指定も…。
下記のコンポーネントでは、input要素をrefとして取得できるようにして、
その値をsubmitへと送るようにしている。

src/components/InputArea.tsx
import * as React from 'react';

// submitの型指定
type InputAreaProps = {
  submit: (text: string) => any;
}

const InputArea: React.FC<InputAreaProps> = ({ submit }) => {
  const textInput = React.createRef<HTMLInputElement>();

  const handleOnClick = (): void => {
    if (textInput.current && textInput.current.value) {
      submit(textInput.current.value);
      textInput.current.value = '';
    }
  }

  return (
    <div>
      <input
        type="text"
        placeholder="テキスト"
        ref={textInput}
      />
      <button onClick={handleOnClick}>Add</button>
    </div>
  )
}

export default InputArea;

refオブジェクトの作成には、React.createRef()関数を使用する。
対象のinput要素は"HTMLInputElement"型を持つので、それをジェネリクスに指定する。
refのオブジェクトは、textInputに格納する。

src/components/InputArea.tsx
// createRefのジェネリクスに、HTMLInputElement型を指定
const textInput = React.createRef<HTMLInputElement>();

次に、textInputをinput要素のrelに指定する。
(refに直接名前を指定する方法は非推奨です。必ずcreateRefを使用してください。)

src/components/InputArea.tsx
<input
  type="text"
  placeholder="テキスト"
  ref={textInput}
/>

これで"textInput.current.value"にアクセスすればいいけれど、エラーになる。
これは、createRefで返されるRefObject型がnullを許容していて、前々回で触れたstrictPropertyInitializationにひっかかるため。

node_modules/@types/react/index.d.ts
interface RefObject<T> {
    readonly current: T | null;
}

(readonlyというのは、読み込み専用のプロパティを示す修飾子です)

これは、if文でtextInput.currentを持つかどうかを判断させることで回避できる。

src/components/InputArea.tsx
// RefObjectのcurrentは、nullの可能性がある
// textInput.currentプロパティがあるかどうかも確認する
const handleOnClick = (): void => {
  if (textInput.current && textInput.current.value) {
    submit(textInput.current.value);
    textInput.current.value = '';
  }
}

まとめ

Redux編に続きます。