Flowのnullチェックとanyへのキャスト

Flowはnullに対するチェックが厳しく、nullを想定していない場所にnullを渡したり、nullが渡される可能性があったりすると、エラーを出してくれる。

例えば以下のコードでFlowを実行すると、エラーが出る。

// @flow

type Owner = {
  name: string,
  age: number,
};

type Repository = {
  id: number,
  isPublic: boolean,
  owner: Owner | null,
};

function showOwnerName(owner) {
  console.log(`This owner is ${owner.name}.`);
}

function showRepositoryData(repository: Repository) {
  console.log(`Id is ${repository.id}.`);
  console.log(repository.isPublic ? 'This is public.' : 'This is not public.');
  showOwnerName(repository.owner);
}

const exampleRepo = {
  id: 23,
  isPublic: true,
  owner: {
    name: 'Tom',
    age: 50,
  },
};

showRepositoryData(exampleRepo);

このプログラムそのものは問題なく動き、ログが表示される。

まずRepositoryというデータ構造があり、idisPublicownerというデータを持っている。
そしてownerは、nameageを持っている。
ただし、全てのRepositoryownerを持っているとは限らず、その値がnullである可能性もある。

この「ownerがnullの可能性がある」というのが、問題になる。

上記のサンプルではownerはnullではないが、型の定義でnullもあり得るとしている以上、プログラム的には、ownerがnullである可能性は否定できない。というか、そういう可能性があるからこそ、owner: Owner | nullと定義しているのだ。

そしてownerがnullのときにowner.nameにアクセスしようとすると、エラーになる。

そのため、上記のコードをFlowでチェックするとエラーになる。
プロパティにアクセスしているけど、そのときにownerがnullである可能性がありますよ、と。

property `name`. Property cannot be accessed on possibly null value

nullチェック

nullかどうかをチェックするようにすれば、エラーは出なくなる。

function showOwnerName(owner) {
  if (owner) console.log(`This owner is ${owner.name}.`);
}

また、仕様上、ownerがnullのときはshowOwnerNameは呼ばれないようになっている、呼ばれてはいけない、というのなら、nullのときは例外を投げるようにするのがいいかもしれない。

function showOwnerName(owner) {
  if (!owner) throw new Error('owner is null.');
  console.log(`This owner is ${owner.name}.`);
}

anyへのキャスト

ownerが確実にnullではない、というケースなら、anyにキャストしてしまうという方法もある。

function showOwnerName(owner) {
  console.log(`This owner is ${((owner: any): Owner).name}.`);
}

このように書くことで、ownerの型を好きなもの(上記の場合Owner)にキャストできる。
ただ、公式ドキュメントThis is unsafe and not recommended.とあるように、型チェックを放棄してしまっているわけで、基本的には使うべきではない。
ただ、こういう書き方も出来ると知っておくと、どこかで役に立つこともあるかもしれない。

参考資料

ReactのコンポーネントにFlowの型をつける

以前の記事でFlowの導入が完了し、Reactを使って書いたコードに型を付けることが出来るようになった。
ここからは実際に、Reactのコンポーネントに型を付けていく。

Flow導入の記事はこちら。

なお、この記事を書いている環境のFlowのバージョンは、0.46.0

対象となるコンポーネントを用意

以下のようなコンポーネントを作成した。
このコンポーネントに、型を付けていく。

class App extends React.Component {
  render() {
    return (
      <div>{this.props.name}</div>
    );
  }
}

<App name="Tom" />のようにnameをpropsとして渡して使うだけの、シンプルなコンポーネント

最初に、このファイルの一番上に// @flowと記述する。
この記述があるファイルのみが、Flowによるチェックの対象となる。

propsに型を付ける

まずはprops.nameに、型をつけてみる。
下記のようにprops:と記述すればいい。

class App extends React.Component {
  props: {
    name: string,
  };
  render() {
    return (
      <div>{this.props.name}</div>
    );
  }
}

プロパティの名前: 型と定義するのが、基本。
型の種類は多数あり、公式ドキュメントで確認できる。
Type Annotations | Flow

上記ではnameの型として文字列を指定しており、<App name={true} />のように文字列以外を渡してFlowを実行すると、エラーになる。

次に、Appを次のように書き換えてみる。

class App extends React.Component {
  props: {
    name: string,
  }
  render() {
    return (
      <div>
        {this.props.name}<br />
        {this.props.age}
      </div>
    );
  }
}

そして、<App name="Tom" age={30} />としてnameageを渡す。

この状態でFlowを実行するとどうなるか。
ageの型が定義されていないとして、エラーになる。
これを解消するには、ageの型も設定すればいい。

props: {
  name: string,
  age: number,
}

このように、型を指定すれば、エラーは消える。
ではこの状態で<App name="Tom" />としてageを渡さなかった場合、どうなるか。
やはりエラーになる。

上記の設定だと、「ageにはかならずnumberが渡される」ことを意味しており、他のデータ型が渡されたときはもちろん、値が渡されなかったときにもエラーとなる。

このような、値が渡されたときの型は決まっているが、値が渡されないこともある、というケースでは、以下のように設定すればよい。

props: {
  name: string,
  age?: number,
}

?がついているが、これはOptional Propertyというもので、このプロパティは文字通りオプショナルになる。
つまり、値を渡しても渡さなくてもよい。
これで、先程のエラーは消える。

Optional object type properties

論理和

要するにOR
これは何も難しいことはなく、プロパティの名前: 型 | 型とすればいい。

type Prop = {
  name: string,
  age: number | string,
};

このように書くと、ageは数値か文字列のいずれか、という指定になる。
そのため<App name="Tom" age="30" />でもエラーにならない。

独自の型定義

自分で型を定義することも出来る。
typeで宣言して、定義する。

下記では、Propという型を定義し、それをApppropsに設定している。

type Prop = {
  name: string,
  age: number,
};

class App extends React.Component {
  props: Prop;
  render() {
    return (
      <div>
        {this.props.name}<br />
        {this.props.age}
      </div>
    );
  }
}

型の定義が長くなるようであれば、このほうが可読性が増すかもしれない。

何より、型を再利用することが出来る。
同じデータ構造が何度も出てくるのなら当然、一箇所で定義して使いまわしたほうがいい。
exportすることも出来るので、ファイルをまたがって使うことが出来る。

stateに型を付ける

Appを拡張してstateやメソッドを追加し、stateに型を付けたのが、以下。

type Prop = {
  name: string,
  age: number,
};

type State = {
  isYouth: boolean,
};

class App extends React.Component {
  constructor(props) {
    super(props);
    this.state = { isYouth: this.isYouth() };
  }
  state: State;
  props: Prop;
  isYouth() {
    return this.props.age < 30;
  }
  render() {
    return (
      <div>
        {this.props.name}<br />
        {this.props.age}
      </div>
    );
  }
}

stateの場合も、propsの場合と何も変わらない。

メソッドにも型をつける

先程の状態でFlowを実行してもエラーにはならないが、メソッドにも型をつけたほうが望ましい。

まず、constructor
引数として渡されるpropsの型を、Propとして指定している。

constructor(props: Prop) {
  super(props);
  this.state = { isYouth: this.isYouth() };
}

次にisYouth。返り値は真偽値であると指定している。

isYouth(): boolean {
  return this.props.age < 30;
}

Stateless Functinal Componenens

Reactのコンポーネントは関数で定義することも出来るが、それに対しては、次のように型を付ける。

const App = (props: { name: string, age: number }) => (
  <div>
    {props.name}<br />
    {props.age}
  </div>
);

参考資料