フック
フックは@types/react v16.8以降でサポートされています。
useState
単純な値に対しては型推論が非常にうまく機能します
const [state, setState] = useState(false);
// `state` is inferred to be a boolean
// `setState` only takes booleans
推論に頼ってきた複雑な型を使用する必要がある場合は、推論された型の使用セクションも参照してください。
ただし、多くのフックはnullっぽいデフォルト値で初期化されるため、型の提供方法が疑問に思うかもしれません。明示的に型を宣言し、共用体型を使用してください
const [user, setUser] = useState<User | null>(null);
// later...
setUser(newUser);
状態がセットアップ直後に初期化され、常に値を持つ場合は、型アサーションを使用することもできます
const [user, setUser] = useState<User>({} as User);
// later...
setUser(newUser);
これは一時的にTypeScriptコンパイラに{}がUser型であると「嘘」をつきます。その後、user状態を設定する必要があります。設定しないと、残りのコードがuserがUser型であるという事実に依存し、実行時エラーが発生する可能性があります。
useCallback
他の関数と同様に、useCallbackの型を指定できます。
const memoizedCallback = useCallback(
  (param1: string, param2: number) => {
    console.log(param1, param2)
    return { ok: true }
  },
  [...],
);
/**
 * VSCode will show the following type:
 * const memoizedCallback:
 *  (param1: string, param2: number) => { ok: boolean }
 */
React < 18の場合、useCallbackの関数シグネチャは、デフォルトで引数をany[]として型付けすることに注意してください
function useCallback<T extends (...args: any[]) => any>(
  callback: T,
  deps: DependencyList
): T;
React >= 18では、useCallbackの関数シグネチャは以下のように変更されました
function useCallback<T extends Function>(callback: T, deps: DependencyList): T;
したがって、次のコードは、React >= 18では「パラメータ 'e' に暗黙的に 'any' 型があります。」エラーが発生しますが、<17では発生しません。
// @ts-expect-error Parameter 'e' implicitly has 'any' type.
useCallback((e) => {}, []);
// Explicit 'any' type.
useCallback((e: any) => {}, []);
useReducer
reducerアクションには判別共用体を使用できます。reducerの戻り値の型を定義することを忘れないでください。そうしないと、TypeScriptがそれを推論します。
import { useReducer } from "react";
const initialState = { count: 0 };
type ACTIONTYPE =
  | { type: "increment"; payload: number }
  | { type: "decrement"; payload: string };
function reducer(state: typeof initialState, action: ACTIONTYPE) {
  switch (action.type) {
    case "increment":
      return { count: state.count + action.payload };
    case "decrement":
      return { count: state.count - Number(action.payload) };
    default:
      throw new Error();
  }
}
function Counter() {
  const [state, dispatch] = useReducer(reducer, initialState);
  return (
    <>
      Count: {state.count}
      <button onClick={() => dispatch({ type: "decrement", payload: "5" })}>
        -
      </button>
      <button onClick={() => dispatch({ type: "increment", payload: 5 })}>
        +
      </button>
    </>
  );
}
reduxのReducerとの使用
reducer関数を記述するためにreduxライブラリを使用する場合、戻り値の型を処理する便利なヘルパーReducer<State, Action>が提供されます。
そのため、上記のreducerの例は次のようになります
import { Reducer } from 'redux';
export function reducer: Reducer<AppState, Action>() {}
useEffect / useLayoutEffect
useEffectとuseLayoutEffectはどちらも**副作用**を実行するために使用され、オプションのクリーンアップ関数を返します。つまり、戻り値を処理しない場合は、型は必要ありません。 useEffectを使用する場合は、関数またはundefined以外を返さないように注意してください。そうしないと、TypeScriptとReactの両方が警告を発します。これは、アロー関数を使用する場合に微妙な場合があります
function DelayedEffect(props: { timerMs: number }) {
  const { timerMs } = props;
  useEffect(
    () =>
      setTimeout(() => {
        /* do stuff */
      }, timerMs),
    [timerMs]
  );
  // bad example! setTimeout implicitly returns a number
  // because the arrow function body isn't wrapped in curly braces
  return null;
}
上記の例の解決策
function DelayedEffect(props: { timerMs: number }) {
  const { timerMs } = props;
  useEffect(() => {
    setTimeout(() => {
      /* do stuff */
    }, timerMs);
  }, [timerMs]);
  // better; use the void keyword to make sure you return undefined
  return null;
}
useRef
TypeScriptでは、`useRef`は、型引数が初期値を完全にカバーしているかどうかによって、読み取り専用または変更可能のいずれかの参照を返します。ユースケースに適した方を選択してください。
オプション1:DOM要素ref
**DOM要素にアクセスするには:** 引数として要素型のみを提供し、初期値として`null`を使用します。この場合、返される参照には、Reactによって管理される読み取り専用の`.current`があります。TypeScriptは、このrefを要素の`ref`プロップに渡すことを期待しています
function Foo() {
  // - If possible, prefer as specific as possible. For example, HTMLDivElement
  //   is better than HTMLElement and way better than Element.
  // - Technical-wise, this returns RefObject<HTMLDivElement>
  const divRef = useRef<HTMLDivElement>(null);
  useEffect(() => {
    // Note that ref.current may be null. This is expected, because you may
    // conditionally render the ref-ed element, or you may forget to assign it
    if (!divRef.current) throw Error("divRef is not assigned");
    // Now divRef.current is sure to be HTMLDivElement
    doSomethingWith(divRef.current);
  });
  // Give the ref to an element so React can manage it for you
  return <div ref={divRef}>etc</div>;
}
divRef.currentがnullにならないことが確実な場合は、非nullアサーション演算子!を使用することもできます
const divRef = useRef<HTMLDivElement>(null!);
// Later... No need to check if it is null
doSomethingWith(divRef.current);
ここで型安全性をオプトアウトしていることに注意してください。レンダーでrefを要素に割り当てるのを忘れた場合、またはrefが参照する要素が条件付きでレンダーされる場合、ランタイムエラーが発生します。
ヒント:使用する`HTMLElement`の選択

オプション2:変更可能な値ref
**変更可能な値を持つには:** 必要な型を提供し、初期値がその型に完全に属していることを確認してください
function Foo() {
  // Technical-wise, this returns MutableRefObject<number | null>
  const intervalRef = useRef<number | null>(null);
  // You manage the ref yourself (that's why it's called MutableRefObject!)
  useEffect(() => {
    intervalRef.current = setInterval(...);
    return () => clearInterval(intervalRef.current);
  }, []);
  // The ref is not passed to any element's "ref" prop
  return <button onClick={/* clearInterval the ref */}>Cancel timer</button>;
}
関連項目
useImperativeHandle
このStackoverflowの回答に基づく
// Countdown.tsx
// Define the handle types which will be passed to the forwardRef
export type CountdownHandle = {
  start: () => void;
};
type CountdownProps = {};
const Countdown = forwardRef<CountdownHandle, CountdownProps>((props, ref) => {
  useImperativeHandle(ref, () => ({
    // start() has type inference here
    start() {
      alert("Start");
    },
  }));
  return <div>Countdown</div>;
});
// The component uses the Countdown component
import Countdown, { CountdownHandle } from "./Countdown.tsx";
function App() {
  const countdownEl = useRef<CountdownHandle>(null);
  useEffect(() => {
    if (countdownEl.current) {
      // start() has type inference here as well
      countdownEl.current.start();
    }
  }, []);
  return <Countdown ref={countdownEl} />;
}
関連項目:
カスタムフック
カスタムフックで配列を返す場合、TypeScriptは共用体型を推論するため、型推論は避けたい場合があります(実際には配列の各位置で異なる型が必要な場合)。代わりに、TS 3.4 constアサーションを使用してください
import { useState } from "react";
export function useLoading() {
  const [isLoading, setState] = useState(false);
  const load = (aPromise: Promise<any>) => {
    setState(true);
    return aPromise.finally(() => setState(false));
  };
  return [isLoading, load] as const; // infers [boolean, typeof load] instead of (boolean | typeof load)[]
}
こうすることで、分割代入時に、分割代入の位置に基づいて正しい型が得られます。
代替:タプル戻り値の型の表明
constアサーションで問題が発生している場合は、関数戻り値の型を表明または定義することもできます
import { useState } from "react";
export function useLoading() {
  const [isLoading, setState] = useState(false);
  const load = (aPromise: Promise<any>) => {
    setState(true);
    return aPromise.finally(() => setState(false));
  };
  return [isLoading, load] as [
    boolean,
    (aPromise: Promise<any>) => Promise<any>
  ];
}
多くのカスタムフックを記述する場合は、タプルの型を自動的に設定するヘルパー関数も役立ちます
function tuplify<T extends any[]>(...elements: T) {
  return elements;
}
function useArray() {
  const numberValue = useRef(3).current;
  const functionValue = useRef(() => {}).current;
  return [numberValue, functionValue]; // type is (number | (() => void))[]
}
function useTuple() {
  const numberValue = useRef(3).current;
  const functionValue = useRef(() => {}).current;
  return tuplify(numberValue, functionValue); // type is [number, () => void]
}
ただし、Reactチームは、3つ以上の値を返すカスタムフックは、タプルではなく適切なオブジェクトを使用することを推奨しています。
フック + TypeScriptに関するその他の情報:
- https://medium.com/@jrwebdev/react-hooks-in-typescript-88fce7001d0d
- https://fettblog.eu/typescript-react/hooks/#useref
React Hooksライブラリを作成する場合は、ユーザーが使用できるように型も公開する必要があることを忘れないでください。