ReactをTypeScriptで書く1: TypeScript編

追記: 2019/12/14
この記事は自分のTypeScript学習用に書いたもので、例があまり参考にならないことも多いです。
学習にはuhyo氏の記事を読むのが手っ取り早いです。
TypeScriptの型入門 - Qiita

create-react-appでの練習

create-react-appにはTypeScript用のテンプレートがある。
コマンド一発でReact + TypeScriptの練習用環境を構築できる。

npx create-react-app ts-sample --typescript

webpackのファイルなどを見たりカスタマイズしたい場合は、ejectする。
create-react-appが隠れてうまいことやってくれていたファイル群を展開してくれる。
(元に戻せないので注意)

yarn eject

TypeScript(目次)

基本
型の種類
any型
union型
文字列リテラル
関数
配列
Objectとinterface
型推論
type alias(型エイリアス)
Intersection Types(交差型)
総称型(ジェネリクス)
列挙型(Enum)
Type Assertion(型アサーション)とType Guard(型の保護)

続き
さらに続き

TypeScript

基本

型を定義するには、変数名の後に: 型名をつける。

let foo: string = 'bar'; // fooには文字列のみ入る

型の種類

  • string
  • number
  • boolean
  • null
  • undefined
  • symbol

JavaScriptと同じ。
プリミティブ型のみ。

any型

any型というのも定義できる。
どんな型が入ってきても怒られない。

let foo: any = 'bar'; // fooにはなんでも入る

any型はただのJavaScriptになってしまうので乱用はしない。
既存プロジェクトのTypeScript化などで一時的に使われたり、ライブラリ側でanyが指定されることが多い。

敗北者のTypeScript - Qiita

関数の引数など、明示的に型を指定しない時にany型として推論されることがある。
ts-config.jsonにはnoimplicitAnyオプションがあり、設定することでこれらのanyを警告してくれる。

また、「なんでもOK」なanyより若干安全な、Unknown型を用いることができる。

union型

いずれかの型が入る予定、または入って欲しい場合にはunion型を使う。
'|'で型を繋げる。

let foo: string | undefined = 'bar'; // fooには文字列かundefinedが入る

※ strictPropertyInitializationについて

上記コードのように、undefinedが入るかもしれない変数を宣言した時、
undefinedのままでは使えないメソッド(lengthなど)を実行させてしまうことがある。
そうした危険性を予測し、あらかじめエラーを出してくれるstrictPropertyInitializationという設定がある。

let name: string | null = null;
name.length; // 「nullの可能性があります」と出てエラー

let comment: string | undefined;
comment.length; // 「undefinedの可能性があります」と出てエラー

上記コードでは、nullやundefinedがlengthを使おうとしているのでコンパイルできない。
下記のように、適切な値を代入することでコンパイルできるようになる。

let name: string | null = null;
name = '太郎'name.length;

let comment: string | undefined;
comment = 'コメント'comment.length;

この設定は、変数の初期化忘れなどに役立ち、Null安全とも呼ばれる。
create-react-appで作成したプロジェクトでは、この設定が有効になっている。

文字列リテラル

いずれかの文字列だけが入ってほしい場合は、文字列リテラルが使える。

let foo: 'bar' | 'baz' = 'bar'; // fooには'bar'か'baz'が入る

関数

関数では、引数と返り値の型が指定できる。
返り値がない場合には、void(一応anyも…)が指定できる。

// addOneには、数値しか渡せない
// 返り値はないので、voidにする
function addOne(num: number): void {
  console.log(num + 1);
}

let foo: number = 3;
addOne(foo); // 4

アロー関数でもできる。

const addOne = (num: number): void => {
  console.log(num + 1);
};

配列

配列は下記のようになる。

const arr: string[] = ['foo', 'bar', 'baz'];

Objectとinterface

オブジェクトも、下記のように記述できる。

const taro: {
  name: string;
  age: number;
} = {
  name: '太郎', // Personのnameなので、stringが入る
  age: 18, // Personのageなので、numberが入る
};

Interfaceを用いることもできる。

interface Person {
  name: string;
  age: number;
}

const taro: Person = {
  name: '太郎', // Personのnameなので、stringが入る
  age: 18, // Personのageなので、numberが入る
};

この'Person'には、nameとageに値(というか型に対応した値)がないとエラーになる。
そこで、interfaceのプロパティ名末尾に'?'をつけると、省略可能なプロパティとなる。

interface Person {
  name: string;
  age?: number; // 省略可能
}

const taro: Person = {
  name: '太郎',
  // ageはなくてもいい
};

型推論

型を定義しないと型推論になる。

let foo = 'bar'; // fooはstringになる

関数で返り値の型が指定されているなら、
引数として渡す予定の変数は型推論させる、ということもできる。
また、返り値の型を省略して型推論させることもできる。

// addOneには、数値しか渡せない
function addOne(num: number): void {
  console.log(num + 1);
}

const foo = 'bar'; // barは型推論でstringになる
addOne(foo); // addOneにstringは渡せないのでエラー

constでの宣言は、正確にはリテラル単位で型推論される。
上記のfooは、'bar'型として扱われ、’bar'以外の値を受け付けない。

export const ADD = 'ADD' // ADDは、'ADD'しか入らない
export const REMOVE = 'REMOVE' // REMOVEは、'REMOVE'しか入らない

type alias(型エイリアス)

type 名前 = 型名で、名前をつけた型を用意できる。
下記なら、Name型にはstringが入ることが期待される。

type Name = string; // Nameはstring

const hifumiName: Name = 123; // 123は数値なのでエラー

Intersection Types(交差型)

あらかじめ定義したinterfaceに、他の型を合成したい時は、交差型というのが使える。
'&'で繋ぐことで、型同士を連結させて新たな型を作ることができる。

// Profileには、nameとageが入る
interface Profile {
  name: string;
  age: number;
}

// SecretProfileには、weightが入る
interface SecretProfile {
  weight: number;
}

// AllProfile型には、ProfileとSecretProfileのプロパティが入る
type AllProfile = Profile & SecretProfile;

const taroProfile: Profile = {
  name: '太郎',
  age: 18,
};

const jiroProfile: AllProfile = {
  name: '二郎',
  age: 51,
  weight: 68, // SecretProfileのプロパティも必要
};

総称型(ジェネリクス)

関数で、型は指定したいけど型には縛られたくないという時がある。

// 引数の値をそのまま返す
function returnArg(arg: string): string {
  return arg;
}

returnArg('文字列'); // '文字列'

上記ではreturnArgという関数を定義して、stringをそのまま返させている。
これだと関数をstring以外で使うことになった場合は、引数や返り値の型をanyにしなければいけない。

そこで、型を引数として受け取ることができる。
これを総称型(ジェネリクス、ジェネリック)という。
ジェネリクスの部分は、関数名の末尾に<引数名>を書く。

// <T>がジェネリクス
// argで任意の型を受け取る、返り値は型推論
function returnArg<T>(arg: T) {
  return arg;
}

returnArg('文字列'); // '文字列'
returnArg(123); // 123

Reactの型定義では、Functional Componentに「React.FunctionComponent」というのがある。
これとジェネリクスを併用することで、propsの型を定義することができる。

// propsの型を定義
type FooProps = {
  id: string;
}

// propsにFooPropsの型を渡す
const Foo: React.FunctionComponent<FooProps> = props => (
  <>
    <h2 id={props.id}>{props.children}</h2>
  </>
);

列挙型(Enum)

Enumは、ウィキペディア曰く識別子をそのまま有限集合として持つ抽象データ型」である。
よくわからないのでコードで書いてみるとこうなる。

enum Foo {
  HOGE,
  FUGA,
  PIYO
}

console.log(Foo.FUGA); // 1

Enumの中にあるHOGE, FUGA, PIYOは、コンパイル後に0, 1, 2の整数となる。
コンパイル結果の詳細は、こちらの記事に記載されている。
世界がEnumから隠した秘密をひとつひとつ見つけていこうな #TypeScript - 角待ちは対空

出てくる数値自体ではなく、'HOGE'や'FUGA'、'PIYO'の3種類の入ったデータとして使う。   具体的には、Actionのtypeをconstで宣言する代わりにenumで定義するなど。
下記のように、文字列リテラルを定義することもできる。

// ActionTypesというenumでADD、REMOVEを定義
export enum ActionTypes {
  ADD = 'ADD',
  REMOVE = 'REMOVE',
}

// interfaceでActionを定義
export interface Action {
  type: ActionTypes;
  num: number;
}

// add
export function add(num: number): Action {
  return ({
    type: ActionTypes.ADD,
    num
  })
}

// remove
export function remove(num: number): Action {
  return ({
    type: ActionTypes.REMOVE,
    num
  })
}

enumをconstで宣言することができる。
これをconst Enum(Enum定数)という。

const enum Foo {
  HOGE,
  FUGA,
  PIYO
}

let fuga = Foo.fuga;

コンパイルすると整数とコメントが残る。

var fuga = 1 /* FUGA */;

どちらにしろあまり使われていないイメージがある。

Type Assertion(型アサーション)とType Guard(型の保護)

下記のコードでは、taroはPerson型でもProduct型でもなく{}になる。
このtaroに、nameプロパティを追加しようとしてもエラーとなる。

interface Person {
  name: string;
  age: number;
}

interface Product {
  name: string;
  price: number;
}

const taro = {}
taro.name = '太郎' // taroは{}なのでnameにはアクセスできない

末尾にas 型の名前を記述することで、taroをその型として扱うことができる。
これをType Assertion(型アサーション)という。

const taro = {} as Person // taroにPerson型を上書き
taro.name = '太郎' // taroはPerson型の持つnameにアクセスできる

下記の記述もあるけれど、JSXでは使用できず、古い書き方となり推奨されない。

const taro = <Person>{} // taroにPerson型を上書き
taro.name = '太郎' // taroはPerson型の持つnameにアクセスできる

明らかに型が違うものはアサーションできない。

const taro = '太郎' as Person // エラー

次にType Guard。
Personだった太郎が商品にも存在する場合、少し面倒になる。
まずはPerson型とProduct型を作成する。

interface Person {
  name: string;
  age: number;
}

interface Product {
  name: string;
  price: number;
}

const taroPerson: Person = {
  name: '太郎',
  age: 18,
}

const taroProduct: Product = {
  name: '太郎',
  price: 1980,
}

Person型には'age'が、Product型には'price'が固有の値として存在する。
これを用いて、太郎がPersonかProductか判定する関数を作成する。

でも、下のような関数だとエラーになる。
PersonかProductが引数に入るのに、欠けているプロパティを関数内で参照しているため。

// taro.ageの有無で判定
function getTaro(taro: Person | Product): string {
  // taro.ageはProductにないのでエラー
  if(taro.age) {
    return `太郎は${taro.age}歳で人`;
  } else {
    // taro.priceはPersonにないのでエラー
    return `太郎は${taro.price}円の商品`;
  }
}

一応、型アサーションを()で囲んで使うといける。

// taro.ageの有無で判定
function getTaro(taro: Person | Product): string {
  // ProuctもPerson型として扱う
  if((taro as Person).age) {
    return `太郎は${(taro as Person).age}歳で人`;
  } else {
    return `太郎は${(taro as Product).price}円の商品`;
  }
}

でもこれだと、毎回型アサーションを記述して冗長な感じになってしまう。
そこで、関数を作成する。

function isTaroPerson(taro: Person | Product): taro is Person {
  return (taro as Person).age !== undefined;
}

なんだこれはという関数で、返り値の型が'taro is Person'となっているのが特徴。
これはtype predicateと言い、型述語と訳されていることが多い。

型述語は引数の名前 is 型名の形式で書く。
関数内で引数taroはPerson型となり、ageが含まれていればtrueを返す。
つまりtaro is PersonなのでPerson型になる。

あとは、この関数を条件分岐に入れるだけでいける。
isTaroPersonがfalseだった型は、Productとして扱われ、priceプロパティを読みに行ける。

function getTaro(taro: Person | Product): string {
  // 引数がPersonかProductかチェック
  if(isTaroPerson(taro)) {
    return `太郎は${taro.age}歳で人`;
  } else {
    return `太郎は${taro.price}円の商品`;
  }
}

まとめ

設定、d.ts編に続きます。