2021年9月1日水曜日

React:React + TypeScript

1.関数コンポーネント

関数コンポーネントです。普通の関数と同じく、引数を型づけしてください。
JSXの要素を返せば戻り値は推論されます。

type AppProps = { message: string }; // interfaceでもよい

const App = ({ message }: AppProps) => <div>{message}</div>;

戻り値を型づけする場合には、JSX.Elementです。
正しい値が返されなければ、エラーが示されます。

const App = ({ message }: AppProps): JSX.Element => <div>{message}</div>;

型エイリアスやインタフェースは定めずに、型注釈を直接書き加えても構いません。
型をほかで使わないときは、この方が簡単です。

const App = ({ message }: { message: string }) =>

<div>{message}</div>;

引数と戻り値をそれぞれ定めるのでなく、関数そのものに型づけすることもできます。

  • コンポーネントの関数をReact.FunctionComponentまたはReact.FCで型づけするのは、TypeScriptの基本テンプレートから除かれるからです。
  • React 18ではReact.FunctionComponentが、デフォルトではchildrenを暗黙で受け取らないように改められるので、将来的にはVoidFunctionComponentは推奨されなくなります。

2.フック

2.1.useState

useStateフックには、可能なかぎり初期値を与えましょう。そうすれば、状態変数の型は推論されるからです。

const [text, setText] = useState('');  // 初期値を与える

初期値を与えなかったとき、「React: フックで値を定めた要素に制御されたコンポーネントを使うよう警告が出る ー Warning: Changing uncontrolled input」という問題も確認されています。

型を組み合わせたいという場合には、useStateにユニオン型をジェネリックで定めればよいでしょう。

const [id, setId] = useState<string | number>(0);

あるいは、型が定めてあっても、初期値があとの初期化処理で決まるとか、値がない場合としてnullを含めたいということもあるかもしれません。そのときも、ユニオン型でnullを加えてください。

const [user, setUser] = React.useState<IUser | null>(null);

// 初期化処理など

setUser(newUser);

######################################

Genericsの簡単な具体例(関数編)

// number型

function test(arg: number): number {

  return arg;

}

// string型

function test2(arg: string): string {

  return arg;

}

test(1); //=> 1

test2("文字列"); //=> 文字列


これをGenericsを使用する事で下記のように書く事が可能です。

test<string>の引数はstring型だけ、またtest<number>の引数はnumber型だけが許されるようになります。

function test<T>(arg: T): T {

  return arg;

}

test<number>(1); //=> 1

test<string>("文字列"); //=> 文字列

//※ Genericsでも型推論ができるので、引数から型が明示的にわかる場合は省略が可能

test("文字列2"); //=> "文字列2"

######################################

nullは型に含めず、useStateの初期値が決まらないという場合には、型アサーションで逃げる手もあります。

const [user, setUser] = React.useState<IUser>({} as IUser);

// 型 'null' の引数を型 'SetStateAction<IUser>' のパラメーターに割り当てることはできません。

// setUser(null);  // nullは認められない

setUser(newUser);  // 型に合致した値のみ設定できる

ただし、型アサーションはTypeScriptに値({})の型を偽っているだけだ、ということにご注意ください。状態変数(user)の値を正しく扱うことは、コードの書き手に委ねられるのです。誤ればランタイムエラーになってしまうかもしれません。

2.2.useReducer

useReducerフックを使うには、リデューサの関数が定められていなければなりません。関数に型づけするのは、リデューサが受け取る状態(state)とアクション(action)のふたつの引数です。戻り値の型は推論されます。

type STORE = { count: number };

type ACTIONTYPE =

    | { type: 'increment' }

    | { type: 'decrement' };

const reducer = (state: STORE, action: ACTIONTYPE) => {

    switch (action.type) {

        case 'increment':

            return { count: state.count + 1 };

        case 'decrement':

            return { count: state.count - 1 };

        default:

            return state;

    }

};


export default reducer;

すると、useReducerフックを使う側では、とくに型の定めはなくて構いません。

import { useReducer } from 'react';

import reducer from './reducer';


const initialState = { count: 0 };

const Counter = () => {

    const [state, dispatch] = useReducer(reducer, initialState);


};

リデューサ関数そのものを、React.Reducerで型づけることもできます。状態(state)とアクション(action)を受け取って、戻り値は状態です。引数の型はジェネリックで与えてください。

import React from 'react';


// const reducer = (state: STORE, action: ACTIONTYPE) => {

const reducer: React.Reducer<STORE, ACTIONTYPE> = (state, action) => {


};

なお、「React + TypeScript: useReducerを使ったカウンターのアプリケーションに型づけする」は、簡単なカウンターの作例をもとに、さらに詳しく解説しました。ご興味がありましたらお読みください。


2.3.判別可能なユニオン型

判別に用いるプロパティが備わった型を複数結ぶと、判別可能なユニオン型(discriminated union)と呼ばれます。

type ACTIONTYPE =

    | { type: 'increment' }

    | { type: 'decrement' };

判別用のプロパティ値が限定されるため、それ以外の値を弾くことができるのです。

const reducer = (state: STORE, action: ACTIONTYPE) => {

    switch (action.type) {

        case 'increment':

            return { count: state.count + 1 };

        case 'decrement':

            return { count: state.count - 1 };

        // 型 '"reset"' は型 '"increment" | "decrement"' と比較できません。

        /* case 'reset':  // エラー

            return { count: 0 }; */

        default:

            return state;

    }

};


2.4.useEffect

useEffectフックでは、はっきりした型づけより、推論される戻り値の型にご注意ください。たとえば、つぎの例です。

useEffect(

     () =>

          window.setTimeout(() => console.log('timeout'), 1000)

     , []

);

アロー関数式=>の本体に波括弧{}なしに1行で書いた文は、そのまま戻り値になります(「アロー関数」の「関数の本体」参照)。ところが、useEffectのコールバック関数は、戻り値はなし(undefined)にするか、「エフェクトのクリーンアップ」関数でなければなりません。そのため、つぎのようなエラーが示されてしまうのです。

Type 'number' is not assignable to type 'void | Destructor'.


クリーンアップ関数がないときは、useEffectのコールバックから値を返さないように注意してください(本体が1行のときは波かっこ{}で括ります)。

useEffect(

    () => {

        window.setTimeout(() => console.log('timeout'), 1000)

    }

    , []

);


2.5.useRef

useRefフックについて、「React+TypeScript Cheatsheets」には型の定めと初期値の与え方で、3つの例が示されています。currentプロパティが書き替えられるか、null値の代入を認めるかどうかが違いです。

// 読み取り専用。

const nullRef = useRef<number>(null);

// 指定した型の値で書き替えられる。nullは不可。

const nonNullRef = useRef<number>(null!);

// 型指定にnullを含める。

const nullableRef = useRef<number | null>(null);


3.イベントハンドラ

イベントハンドラのもっとも簡単な定め方は、要素のイベント属性(onChange)に直に書き加えることです。引数のイベント(event)の型は推論されますので、注釈が要りません。

import React, { useState } from 'react';


function App() {

    const [text, setText] = useState('');

    return (

        <div>

            <input type="text"

                value={text}

                onChange={(event) => setText(event.currentTarget.value)}

            />

        </div>

    );

}


ハンドラ関数(handleChange)を分けて定めると、通常の関数と同じように引数と戻り値を型づけすることになるでしょう(戻り値は推論させても構いません)。

function App() {


    const handleChange = (event: React.FormEvent<HTMLInputElement>): void => {

        setText(event.currentTarget.value);

    };

    return (

        <div>

            <input type="text"

                value={text}

                // onChange={(event) => setText(event.currentTarget.value)}

                onChange={handleChange}

            />

        </div>

    );

}

さらに、ハンドラ関数そのものを(この場合FormEventHandlerで)型づけることもできます。定められるのは、前の例と同じく引数と戻り値の型です。けれど、それぞれをアラカルトで決めるのでなく、セットにした関数の型で示す方がより明確になります。

function App() {


    // const handleChange = (event: React.FormEvent<HTMLInputElement>): void => {

    const handleChange: React.FormEventHandler<HTMLInputElement> = (event) => {


}