最近になって React + TypeScript に入門したので、自分へのメモがてら、基本的なことをまとめてみました。
前提
TypeScript 初心者です。間違いや違和感等々ありましたらコメントで教えていただけると助かります。
JavaScript・React はある程度理解していることが前提です。
TypeScript・React の環境構築やtsconfig.json
の設定には触れません。
TypeScript の基礎
「明示的な型定義」と「型推論」
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];
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;
};
オブジェクトの型指定における Interface
と Type 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> {
value: 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 canvas3 = <HTMLCanvasElement>document.getElementById("canvas"); // canvas3 : HTMLCanvasElement
// NG
// このような指定もできるため、値とアサーションの型が一致するとき以外は使用してははいけない。
const possible = true as false;
型推論について
明示的な型定義をしなくても、TypeScript はいい感じに型を推論してくれます。
基本的に型推論が期待する結果と違う時のみ、明示的に型定義をすればいいと思います。
また、const
とlet
で型推論の結果が異なる場合があるので、気をつける必要があります(基本的には cosnt
と let
の挙動の違いが反映されるイメージです)。
// 型推論 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.FC
と React.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
における state
と action
に対して明示的な型定義をしています。
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