share facebook facebook facebook twitter twitter menu hatena pocket slack

2021.07.06 TUE

React + TypeScript に入門したので基本をまとめてみた

遠藤 優輝

WRITTEN BY 遠藤 優輝

最近になって React + TypeScript に入門したので、自分へのメモがてら、基本的なことをまとめてみました。

前提

TypeScript 初心者です。間違いや違和感等々ありましたらコメントで教えていただけると助かります。
JavaScript・React はある程度理解していることが前提です。
TypeScript・React の環境構築やtsconfig.jsonの設定には触れません。

TypeScipt の基礎

「明示的な型定義」と「型推論」

TypeScript の型定義には明示的な型定義型推論があります。
明示的な型定義は人間が型を指定するのに対し、型推論は TypeScript がいい感じに型を推論してくれます。
実際のコーディングでは型推論を使いつつ、必要な時だけ明示的な型定義をすればいいと思いますが、この記事では型の理解を手助けするために明示的な型定義を使用していきます。

基本的な型定義

明示的な型定義の方法です。

let str: string = "hoge"; // string型

上記の形をアノテーション(型注釈)といいます。他にも明示的な型定義にはアサーションという方法がありますが、アサーションについては後ほど記載します。

代表的なプリミティブ 型

一般的に使用頻度の高い代表的なプリミティブ型です。

let str: string = "hoge"; // string型
let num: number = 1; // number型
let bool: boolean = true; // boolean型

null / undefined 型

null と undifined にはそれぞれ固有の型が用意されています。下記のように宣言することはできますが、単体ではあまり使いどころがないかもしれません。

let n: null = null; // null 型
let u: undefined = undefined; // undefined 型

リテラル 型

プリミティブ型のうちの特定の値だけを許容する型になります。

const str: "hoge" = "hoge"; // "hoge"
const one: 1 = 1; // 1
const trueFlg: true = true; // true

// let でも指定した値以外は再代入不可
let two: 2 = 2;
two = 3; // エラー

Union Type(Union Type については後ほど説明)と併用することで、より便利な型定義を実現できます。

let color: "red" | "green" | "blue" = "red";
color = "green";
color = "yellow"; // エラー

Any 型

型が不明な時に型チェックを無効にする型になります。TypeScript の恩恵が受けられないため。可能な限り使わないようにするのが吉です。

let value: any = 0;
value = "hoge"; //これでもコンパイルが通る

Void 型

値を返さない関数の戻り値で使います。

const func = (): void => {
  console.log("hoge");
};

Array 型

配列の型です。

const strArr: string[] = ["red", "green"]; // 配列の中には string 型
const arrNum: number[] = [1, 2]; // 配列の中には number 型

strArr[0] = "blue";
strArr[0] = false; // エラー

Tuple 型

要素の数がわかっている配列を表現できる型です。

const tupleArr: [string, number] = ["Hanako", 26];
tupleArr[1] = "Taro"; // エラー

Object 型

オブジェクト型は非プリミティブな型を表します。

const obj: {
  name: string;
  age: number;
} = {
  name: "Taro",
  age: 20,
};
obj.name = "Sato";
obj.birthday = "1990/1/1"; // エラー

Interface

Interface を使うとオブジェクト型に名前をつけることができます。

interface Obj {
  name: string;
  age: number;
}
const myObj: Obj = {
  name: "Hanako",
  age: 18,
};

Type Alias

型に名前をつける方法として Type Aliasもあります。
Interfaceはオブジェクト専用ですが Type Aliasはオブジェクト以外にも使用可能です。

type Location = string;
let location: Location = "Saitama";

type Obj = {
  name: string;
  age: number;
};
const myObj: Obj = {
  name: "Takashi";
  age: 28;
};

オブジェクトの型指定における InterfaceType Aliasの違いについてはこの記事では記載しませんが、公式ドキュメントに記載がありましたので、ご参考までに。
https://www.typescriptlang.org/docs/handbook/2/everyday-types.html#differences-between-type-aliases-and-interfaces

関数の型

引数と戻り値に型定義をします。

const sampleFunc = (arg: string): string => {
  return arg;
};
console.log(sampleFunc("fuga")); // "fuga"

// 戻り値がないときは void 型を指定
const sampleEchoFunc = (arg: string): void => {
  console.log(arg);
};
sampleEchoFunc("hoge"); // "hoge"

?を使うこと引数がオプショナルな型定義を実現できます。
オプショナルな型定義では値が undefiend になる可能性があるので、そのことを考慮して処理を書く必要があります。

interface posOptions {
  xPos?: number; // xPos : number | undefined
  yPos?: number; // xPos : number | undefined
}

const getPosition = (opts: posOptions) => {
  // xPost と yPos が undefined になる可能性も考えた処理を書く必要がある
  let xPos = opts.xPos === undefined ? 0 : opts.xPos;
  let yPos = opts.yPos === undefined ? 0 : opts.yPos;
  // ...
};

getPosition(); // 引数がなくても関数を呼び出せる
getPosition({ xPos: 1 });
getPosition({ xPos: 1, yPos: 1 });

Promise 型

Promise<>の形で戻り値を表します。

const PromiseFunc = (arg: string): Promise<void> => {
  return new Promise(resolve => {
    setTimeout(() => {
      resolve(arg);
    }, 5000);
  }).then(result => {
    console.log(result);
  });
};

PromiseFunc("Hello World"); // 5秒後に "Hello World"

Union Type

複数の型のうち、どれか一つの型にに当てはまる場合に使います。いわゆる OR 的な使い方です。

const printId = (id: number | string) => {
  console.log("Your ID is: " + id);
};
// OK
printId(101);
// OK
printId("202");

型の結合

&を使うことによって型を結合することができます。

type Sample1 = {
  hoge: string;
};

type Sample2 = {
  fuga: string;
};

type CombineSample = Sample1 & Sample2;

// type CombineSample = {
//   hoge: string;
//   fuga: string;
// };
// 上記と同等になる。

typeof と keyof

typeofを使用すると既存の変数から型の定義を取得できます。
keyofを使用するとオブジェクトの key をリテラルユニオン(リテラル型と Union Type の併用)として取得できます。

const obj = {
  fistName: "Taro",
  lastName: "Yamada",
};
const myobj: typeof obj = {
  fistName: "Hanako",
  lastName: "Satou",
  age: 20, // age は obj に存在しないのでエラーとなる
};

type objType = { fistName: string; lastName: string };
// objType のプロパティを String Union Type として景勝
const key: keyof objType = "fistName"; // key : "fistName" | "lastName"

typeof を型ガードとして使うこともできます。

function printId(id: number | string) {
  if (typeof id === "string") {
    // string 型の時の処理
    console.log(id.toUpperCase());
  } else {
    // number 型の時の処理
    console.log(id);
  }
}

ジェネリクス

ジェネリクスを使用すると型の確定を遅延することができます。

// valueの型がまだ決まっていない
interface GSample<T> {
  vakue: T;
}

// valueの値は string 型になる
const sampleString: GSample<string> = {
  value: "hoge",
};

// valueの値は number 型になる
const sampleString: GSample<number> = {
  value: 9,
};

関数定義でジェネリクスを使うと、関数宣言時に引数の型を未確定にできます。

// 引数の型は決まってない
const GenericsFunc = <T>(arg: T): T => {
  return arg;
};

// 引数の型は string
const result1 = GenericsFunc<string>("hoge");

// 引数の型は number
const result2 = GenericsFunc<number>(10);

アロー関数でジェネリクスを書く、かつ tsx ファイルの場合は extends をつける必要があります。でないと JSX の文法でエラーが出ます。

// tsx ファイルの時は extends をつける
const GenericsFunc = <T extends {}>(arg: T): T => {
  return arg;
};

アサーション

TypeScirpt で推論された型を任意の型に上書きしたい時に使います。アサーションには as<> を使う 2 種類の方法があります。
as<>は基本的に同等ですが、<> は JSXの構文と衝突する可能性があるため asが推奨されます。
実際の値と関係なく型定義ができてしまうため、値の型とアサーションの型が一致するとき以外は使用してはいけません。

const canvas1 = document.getElementById("canvas"); // canvas1 : HTMLElement | null
const canvas2 = document.getElementById("canvas") as HTMLCanvasElement; // canvas2 : HTMLCanvasElement
const canvas2 = <HTMLCanvasElement>document.getElementById("canvas"); // canvas3 : HTMLCanvasElement

// NG
// このような指定もできるため、値とアサーションの型が一致するとき以外は使用してははいけない。
const possible = true as false;

型推論について

明示的な型定義をしなくても、TypeScript はいい感じに型を推論してくれます。
基本的に型推論が期待する結果と違う時のみ、明示的に型定義をすればいいと思います。
また、constletで型推論の結果が異なる場合があるので、気をつける必要があります(基本的には cosntlet の挙動の違いが反映されるイメージです)。

// 型推論 const と let の違い
let color = "red"; // color は string 型
const color = "red"; // color は "red" のみ

// 明示的な型定義を使用したほうがいい場合の例
let color: "red" | "green" | "blue" = "red"; // color は "red", "green", "blue" のどれか
const canvas = document.getElementById("canvas") as HTMLCanvasElement; // canvas : HTMLCanvasElement

TypeScript がどの型を推論しているかというのは、各エディタのヒントなどで確認できます。
(以下は Visual Studio Code で確認している様子)

React と TypeScript

関数コンポーネント

TypeScript における関数コンポーネントの基本的な書き方を以下に示します。

基本形

React.FCという React 独自の型を使用します。React.FCはジェネリクス型なので Props を<>で型定義できます。

import React from "react";

type Props = {
  name: string;
};
const Sample: React.FC<Props> = ({ name }) => {
  return <div>Hello {name}!</div>;
};

export default Sample;

React.FC ではなく React.VFCを使ったほうがいいという説もあります。

React.FCReact.VFC の違いについてはこの記事では記載しませんが、詳しく知りたい方は以下の記事に載っていましたので、ご参考までに。
Function Components | React TypeScript Cheatsheets

children を受け取る

childrenを受け取る時は React.ReactNodeを使います。

import React from "react";

type Props = {
  children: React.ReactNode;
};
const Sample: React.FC<Props> = ({ children }) => {
  return <div>{children}}</div>;
};

export default Sample;

オプショナルな Props を渡す

?を使うことでオプショナルな型定義ができます。オプショナルな値では undefinedの可能性が生じることを考慮する必要があります。

// x も y オプショナル
type Props = {
  x?: number; // x : number | undefined
  y?: number; // y : number | undefined
};

// オプショナルな値は undefined の可能性があるので初期値を設定している
const CalcSum: React.FC<Props> = ({ x = 0, y = 0 }) => {
  const sum = x + y;
  return <div>合計: {sum}</div>;
};
const App: React.FC = () => {
  return <CalcSum x={2} />;
};

EventCallback と型定義

props で受け取る EventCallback の定義が必要です。以下に onChnageの時の例を示します。

type InputProps = {
  value: string;
  onChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
};
const Input: React.FC<InputProps> = ({ onChange, value }) => {
  return (
    <div>
      <input type="text" onChange={onChange} value={value} />
    </div>
  );
};

const App: React.FC = () => {
  const [inputValue, setInputValue] = useState("");
  const onChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    setInputValue(e.target.value);
  };
  return (
    <div>
      <div>{inputValue}</div>
      <Input value={inputValue} onChange={onChange} />
    </div>
  );
};

その他の EventCallback については数が多くなるのでこの記事では記載しませんが、以下の Qiita 記事が非常にまとまっていたので、詳しく知りたい方はそちらをご参照ください。投稿者の方、ありがとうございます。
any 型で諦めない React.EventCallback
下記は同記事からの一部引用になります。

  type Props = {
  onClick: (event: React.MouseEvent<HTMLInputElement>) => void;
  onChange: (event: React.ChangeEvent<HTMLInputElement>) => void;
  onkeypress: (event: React.KeyboardEvent<HTMLInputElement>) => void;
  onBlur: (event: React.FocusEvent<HTMLInputElement>) => void;
  onFocus: (event: React.FocusEvent<HTMLInputElement>) => void;
  onSubmit: (event: React.FormEvent<HTMLFormElement>) => void;
  onClickDiv: (event: React.MouseEvent<HTMLDivElement, MouseEvent>) => void;
};

Hooks と型

Hooks も型推論が効くので、必要な時だけ明示的な型定義をします。

useState

明示的な型定義をしたいときはアサーションを使います。

const [val, setVal] = useState(false); // これで問題ない
const [val, setVal] = useState<boolean | null>(null); // Nullable にしたい場合はアサーションを使う

useEffect

useEffectを使うときには戻り値に気をつけます。
関数 or undefined 以外は返さないようにします(ただこれは TypeScript を使ってなくても同様です)。

// NG
useEffect(
  () =>
    setTimeout(() => {
      /* ... */
    }, 1000),
  []
);

// OK
useEffect(() => {
  setTimeout(() => {
     /* ... */
  }, 1000);
}, []);

useRef

useRefの場合は初期値に null が入るケースが多いのでアサーションで指定しています。

const App: React.FC = () => {
  // 明示的な型定義が必要
  const ElementRef = useRef<HTMLDivElement>(null);

  useEffect(() => {
    // 型ガードが必要
    if (!ElementRef.current) throw Error("ElementRef is not assigned");
    console.log(ElementRef.current.getBoundingClientRect());
  }, []);

  return <div ref={ElementRef}>app</div>;
};

useReducer

reducerにおける stateaction に対して明示的な型定義をしています。
action をACTIONTYPEで定義。
state の型は typeofを使うことでinitialStateの型を流用できます。

const initialState = { count: 0 };

type ACTIONTYPE =
  | { type: "increment"; payload: number }
  | { type: "decrement"; payload: number };

const reducer = (state: typeof initialState, action: ACTIONTYPE) => {
  switch (action.type) {
    case "increment":
      return { count: state.count + action.payload };
    case "decrement":
      return { count: state.count - action.payload };
    default:
      throw new Error();
  }
};

const App: React.FC = () => {
  const [state, dispatch] = useReducer(reducer, initialState);
  return (
    <div>
      Count: {state.count}
      <button onClick={() => dispatch({ type: "decrement", payload: 1 })}>
        Count Down
      </button>
      <button onClick={() => dispatch({ type: "increment", payload: 1 })}>
        Count Up
      </button>
    </div>
  );
};

useContext

Provider のvalueにセットする値の型定義以外は特に目新しいものはありません。
createContextで初期値をnullにする場合はアサーションが必要です。

interface AppContextInterface {
  name: string;
  age: number;
}
const sampleAppContext: AppContextInterface = {
  name: "Yamada Taro",
  age: 20,
};

const AppContext = createContext<AppContextInterface | null>(null);

const ChildComponent: React.FC = () => {
  // useContext(AppCtx)! の ! は not-null のアサーション
  const appContext = useContext(AppContext)!;

  return (
    <p>
      {appContext.name}は{appContext.age}歳です。
    </p>
  );
};

const App: React.FC = () => {
  return (
    <AppContext.Provider value={sampleAppContext}>
      <ChildComponent />
    </AppContext.Provider>
  );
};

まとめ

今回は基本的なことについてまとめてみましたが、当然ながらカバーしきれていない範囲も多くあります。
ただ、この記事にまとめたことは React + TypeScript において使用頻度の高いものだと思うので、ゼロから学ぶ際やチートシート的な使い方として参考にしていただけると幸いです。
また TypeScript は公式ドキュメントが充実しており初心者にもわかりやすく書かれていると感じたので、読んだことのない方は是非一度読んでみることをお勧めします。

参考 URL

TypeScript: TypeScript 学習の第一歩
React TypeScript Cheatsheets | React TypeScript Cheatsheets
Introduction – TypeScript Deep Dive
any 型で諦めない React.EventCallback

元記事はこちら

https://qiita.com/yuki-endo/items/124d5c7398da8fadc8e7

遠藤 優輝

遠藤 優輝

streampackチーム所属。グミが好きです。

cloudpack

cloudpackは、Amazon EC2やAmazon S3をはじめとするAWSの各種プロダクトを利用する際の、導入・設計から運用保守を含んだフルマネージドのサービスを提供し、バックアップや24時間365日の監視/障害対応、技術的な問い合わせに対するサポートなどを行っております。
AWS上のインフラ構築およびAWSを活用したシステム開発など、案件のご相談はcloudpack.jpよりご連絡ください。