2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

1. はじめに

  • 記事の目的と前提
    ReactでのTypescriptを実際に学んでわかったことをメモしておきます。
    また、私のような初学者向けに適宜解説を残します。
  • 対象読者
    React、JavaScriptの基本的な使い方を理解しており、Typescriptについて学びたい方

2. 環境構築

2.1. TypeScript設定

tsconfig.jsonが初期の状態だと、厳密な型チェックがあまりないので、追加してより型安全に書けるようにします。初めてTypescriptに触れる方はこの先の説明部分がわからないと思いますが、学習を進めてみてわかるようになったとき、また見返してみてください。

tsconfig.json の主な設定項目

"strict": true:以下6つの型チェック機能を有効化します。まずはこれをtrueにしてみましょう。何が有効になるのか解説します。

  • noImplicitAny:型推論で any になるケースをエラーにする
example.ts
function fn(x) {  // x の型が不明 → any → エラー
  return x * 2;
}
function fn2(x: number) {  // 明示的に number と書けば OK
  return x * 2;
}

  • strictNullChecks:null/undefined をそれぞれ別の型として扱う
example.ts
let s: string;       // s は必ず string(undefined や null は不可)
s = null;            // エラー
let opt: string | null = null;  // null を許可したい場合は union で明示

let s2: string;
s = undefined;       // エラー
let opt: string | undefined = undefined;  // undefined を許可したい場合は union で明示

let Null: null = undefined;          // エラー
let Undefined: undefined = null;     // エラー
  • strictFunctionTypes:関数の引数の型が完全一致していなければエラー。関数をPropsに渡す時に役立ちます。
example.ts
type Animal = { name: string }
type Dog = { name: string; age: number };

// Dog型の引数を受け取り、年齢をログに出す関数
let handleDog = (dog: Dog) => {
  console.log(dog.age)
};

// Animal型の引数を受け取り、名前をログに出す関数
let handleAnimal = (animal: Animal) => {
  console.log(animal.name)
};

// 関数の代入を試す
handleDog = handleAnimal ;    // 引数の型が異なるのでエラー

  • strictBindCallApply:.bind / .call / .apply メソッド使用の際に、渡される引数の型と数をチェック
example.ts
function sum(a: number, b: number) { return a + b; }
sum.call(null, 1);      // 第二引数がない → エラー
sum.apply(null, [1, 2, 3]); // 引数が多すぎ → エラー
const f = sum.bind(null, 'x', 2); // 'x' は number ではない → エラー

  • strictPropertyInitialization:クラスで宣言したプロパティがコンストラクタ内で必ず初期化されているかをチェック
example.ts
class User {
  name: string;         // コンストラクタで name を設定しないとエラー
  constructor() {
    name = 'Alice';  // ← これがないとエラー
  };
};

  • alwaysStrict:出力される JS の先頭に自動で 'use strict'; を挿入
example.ts
'use strict';
function foo() {  };

コードの品質向上・便利系

  • "noUnusedLocals": true:未使用のローカル変数をエラー化
example.ts
function foo() {
  const x = 123;  // ← 使われていないのでエラー
  const y = 456;
  return y;
};

  • noUnusedParameters": true:未使用の関数パラメータをエラー化
example.ts
function greet(name: string, age: number) {  // ← `age` が使われていないためエラー
  console.log(`Hello, ${name}`);  
};  
  • noImplicitReturns": true
    すべてのコードパスで値を返す(または返さない)ことを保証されていなければエラー
example.ts
function fn(x: number) {
  if (x > 0) {
    return x;
  };
  // ここで戻り値がない ⇒ エラー
};

  • "forceConsistentCasingInFileNames": true:ファイル名の大文字・小文字の不一致を検出
example.ts
import { Foo } from './myComponent';  // 実ファイル名が `Mycomponent.tsx` だとエラー

2.2. 拡張機能

今回はTypescriptに関する拡張機能は使用しませんでした。

3. 基本の型定義

TypeScriptは推測が可能な場合に型を補完する型推論を行います。
以下で説明していくのは型を明示的に記載する型注釈です。基本的には推論が効くところはすべて型推論に任せるので、明示的に書くことは少ないですが、まずはどのような型があるか理解していきましょう。
また、TypeScriptにおける型付けのすべてを網羅するものではありません。クラスに関する型とかは取り上げていません。

3.1. プリミティブ型

TypeScriptにおける基本的な型を解説します。

  • string :文字列型
exampl.ts
let message: string = "こんにちは";
  • number :数値型
example.ts
let age: number = 25;
let height: number = 170.5;
  • boolean:真偽値型
example.ts
let isAdmin: boolean = true;
  • null/undefined: null/undefined型
example.ts
let nothing: null = null;
let notDefined: undefined = undefined;
  • any:なんでも入る(TypeScriptの恩恵を受けられない)のでjs→tsに移行するとき、
    型エラー部分にひとまずanyを書いて徐々に修正するといった使い方をします。
example.ts
let anything: any = "Hello";
anything = 123;
anything = true;
  • unknown: 使用する際にガードが必要なany。
    なんでも入るという部分はanyと同じですが、使う際に型が絞り込まれている必要があります。この絞り込みを型ガードといいます。
    下記例では文字列をすべて大文字に変えるtoUpperCase()を使用していますが、
    if文の条件にtypeof 変数 === 希望の型をいれることで、valueがstring型であることを約束してtoUpperCase()を使うようにします。
example.ts
let value: unknown = "text";

if (typeof value === "string") {
  console.log(value.toUpperCase()) // 型ガードしないと使えない
};

3.2. 配列

  • string[]:文字列の配列
example.ts
let fruits: string[] = ["apple", "banana", "grape"];
  • number[]:数値の配列
example.ts
let scores: number[] = [90, 80, 100];
  • タプル型:要素数・順番・型が固定された配列
example.ts
const p: [number, number] = [10, 20];       // OK
const p2: [number, number] = [10, "top"];   // ❌ エラー

3.3. オブジェクト型

  • オブジェクト型:内包するプロパティの型をそれぞれ指定
example.ts
let user: { name: string; age: number } = {
  name: "Taro",
  age: 30
};

また、オブジェクトに限った話ではないですが、項目が増えると見づらくなるので、型部分だけを分離して定義することができます。

ex.型データの定義
example.ts
type User = {
  name: string
  age: number
};

let me: User = { name: "Yuki", age: 27 };

さらに、型の定義はtypeinterfaceの2つがあります。細かい違いはありますが、interfaceはオブジェクトにしか使えないと覚えておけば大丈夫です。Propsの型にはinterfaceを使います。

ex.typeとinterface
example.ts
type User = { name: string; age: number };
let me: User = { name: "Yuki", age: 27 };

interface User2 { name: string; age: number };
let me: User2 = { name: "Akari", age: 25 };

3.4 関数型

  • 関数型: 引数と戻り値にそれぞれ型を定義
ex.引数と戻り値のある関数の型
example.ts
type Greet = (name: string) => string;  

const sayHello: Greet = (name) => {
  return `Hello, ${name}`
};
引数が無い場合は()を空のままに、戻り値が無い場合は`void`と書きます。 ReactのonClickで走らせる関数によく使う型です。
ex.引数、戻り値が無い関数の型
example.ts
type NoFunction = () => void;

const greet: NoFunction = () => {
  console.log("こんにちは"); //greet関数はログに"こんにちは"を出力する関数で何もreturnしない
};

3.5. ユニオン/リテラル/インターセクション/条件型

  • ユニオン型:複数の型の内どれか一つを受け入れる
    後に紹介するcontextやnull,undefinedの型付けなどでよく使います。

|←この縦棒(パイプとかバーティカルバーとか)を許容したい型の間に書きます。
そうすることで変数一つを異なる型の値で書き換えられるようになります。

example.ts
let userId: string | number
userId = "abc123"  // OK
userId = 456       // OK
userId = true      // ❌ エラー(stringかnumberでない)

以下3つはあまり触れられていないので紹介だけにします。

  • リテラル型:値を制限する
example.ts
let dir: "left" | "right" | "center"
dir = "left"     // OK
dir = "bottom"   // ❌ エラー
  • インターセクション型:複数の型を合成してすべてを満たす型を作る
example.ts
type Person = { name: string }
type Employee = { employeeId: number }

type Staff = Person & Employee  //2つの型を合成

const s: Staff = {
  name: "Taro",
  employeeId: 123
}
  • 条件型:条件分岐で動的に型を変える
example.ts
type IsString<T> = T extends string ? "Yes" : "No"

type A = IsString<string>  // "Yes"
type B = IsString<number>  // "No"

4. Reactコンポーネントの型付け

では、ここからReactでTypeScriptを使って型安全に書けるよう解説します。

4.1. 関数コンポーネント

基本の関数コンポーネントに型を付けたものを下に書きました。JSXの要素を返す関数であることを明示的に書いています。

ex.コンポーネントの型(JSX.Element)
Example.tsx
const Hello = (): JSX.Element => {
  return <p>Hello, world!</p>
}
また、関数コンポーネントに型を付ける場合、こちらの方法もあります。 Reactに用意されている関数コンポーネントの型(React.FunctionComponent)を使う方法です。 これを使うとpropsに`children`が自動で含まれてしまい、意図しない動作を招く恐れがあるので使用を避ける人もいます。
ex.コンポーネントの型(React.FC)
Example.tsx
import React from "react"; // 使用時の型:React.FC
// import { FC } from "react";  // 使用時の型:FC

const Hello: React.FC = () => {
  return <p>Hello, world!</p>
};

上記で明示的に型を付けてきましたが、propsが無い場合は型推論によって「この関数はJSXを返すんだな~」と理解してくれるので、実際に書くのは型注釈部分を取ったこれだけです。.jsxの時と変わりません。

ex.型推論されたコンポーネント
Example.tsx
const Hello = () => {
  return <p>Hello, world!</p>
};

4.2. Props の型定義

4.2.1 anyを避ける

React の関数コンポーネントで props(親から渡される値) を使うときは、型推論が効かず、なんでも入るany型にされてしまいます。そのため、型を明示的に定義する必要があります。

型は typeinterface を使って定義し、引数の右側に書きます。

ex.propsの型定義
Child.tsx
interface GreetingProps = {
  name: string;
  isFormal?: boolean;  // オプショナル
}

const Greeting = ({ name, isFormal = false }: GreetingProps) => {
  return <p>{isFormal ? `Hello, ${name}` : `Hi, ${name}`}</p>
}

export default Greeting;

4.2.2 オプショナル

これはjsの話ですが、isFormalに" ? "がついています。これはオプショナルと言ってあってもなくても良いプロパティということになります。
以下のようにisFormalを渡さなくてもエラーになりません。

ex.親コンポーネントでの呼び出し
Parent.tsx
import Greeting from ./Child;

const Parent = () => {
return <Greeting name="Takashi"> //isFormalが無くてもエラーにならない
}

GreetingコンポーネントのpropsでisFormal = falseとしているので、isFormalがpropsとして渡ってこなかった場合は、isFormalfalseになります。

5. State・Context・Ref の型

5.1. useState(基本の型)

useStateにおける基本の型は、明示的に書くと以下のようになります。

ex.stateの型(型注釈)
Example.tsx
// 文字列
const [name, setName] = useState<string>("Takashi");
const [inputValue, setInputValue] = useState<string>("");

// 数値
const [age, setAge] = useState<number>(25);
const [count, setCount] = useState<number>(0);

// 真偽値
const [isOpen, setIsOpen] = useState<boolean>(false);
const [isClose, setIsClose] = useState<boolean>(true);

しかし、初期値を見て推論をしてくれるので...

ex.stateの型(型注釈)
Example.tsx
// 文字列
const [name, setName] = useState("Takashi");
const [inputValue, setInputValue] = useState("");

// 数値
const [age, setAge] = useState(25);
const [count, setCount] = useState(0);

// 真偽値
const [isOpen, setIsOpen] = useState(false);
const [isClose, setIsClose] = useState(true);
このようにjsの時と変わらない記述で問題ありません。

5.2. useState(推論が効かない場合)

ログインユーザーがいない状態、選択項目が未選択など、
nullundefinedを初期値にする場合は注意が必要です。

ex.推論が効かないstateの初期値
Example.tsx

type User = {
  id: number
  name: string
}

// undefined を初期値にする
const [currentUser, setCurrentUser] = useState<User | undefined>(undefined);

// nullを初期値にする
const [selectedItem, setSelectedItem] = useState<string | null>(null);
先ほどと異なり、<>の中に型を2つ書いています。 当記事の3.5で説明した**ユニオン型**です。 初期値のnull/undefinedとそのstateが更新された場合の型を書く必要があります。

これを先ほどのように省略してしまうと...

Example.tsx
// undefined を初期値にする
const [currentUser, setCurrentUser] = useState(undefined);  // ❌

// nullを初期値にする
const [selectedItem, setSelectedItem] = useState(null);  // ❌

初期値から推論をするのでcurrentUserにはundefined、selectedItemにはnullしか入れられず、値を更新するstateの目的を果たせません。
そのため、明示的に書く必要があります。

5.3. useRef

refは初期値としてnullがよく使われるので、注意点を抑えて安全に書きましょう。

refの型は初期値であるnullとセットしたHTML要素に合わせたものとなります。
今回はinputにセットしているので、inputRefにはHTMLInputElementと書きます。
その他HTML要素に対する型を一部紹介します。

HTML 要素 TypeScript 型名
<div> HTMLDivElement
<span> HTMLSpanElement
<input type="text"> HTMLInputElement
<textarea> HTMLTextAreaElement
<button> HTMLButtonElement
<form> HTMLFormElement
<img> HTMLImageElement
<a>(アンカータグ) HTMLAnchorElement

また、inputRef.currentに" ? "がついており、オプショナルとなっています。
nullに対してfocus()を使うことはできないので、handleClick実行時に「input要素があればフォーカスを当ててね」という記述にします。

ex.ボタンクリックでinputにフォーカスが当たる関数コンポーネント(オプショナル)
Example.tsx
import { useRef } from "react"

const InputFocus = () => {
  const inputRef = useRef<HTMLInputElement | null>(null)

  const handleClick = () => {
    inputRef.current?.focus()  //inputRef.currentがnullでなければフォーカス
  }

  return (
    <div>
      <input ref={inputRef} type="text" />
      <button onClick={handleClick}>フォーカスを当てる</button>
    </div>
  )
}

オプショナルの代わりに3.1のunknown型で紹介した型ガードをして使うパターンもあります。
複数の処理を書く場合はこちらが向いています。

ex.ボタンクリックでinputにフォーカスが当たる関数コンポーネント(型ガード)
Example.tsx
import { useRef } from "react"

const InputFocus = () => {
  const inputRef = useRef<HTMLInputElement | null>(null)

  const handleClick = () => {
    // 型ガード:inputRef.current が null でない場合に実行
    if (inputRef.current !== null) {
      inputRef.current.focus()
    }
  }

  return (
    <div>
      <input ref={inputRef} type="text" />
      <button onClick={handleClick}>フォーカスを当てる</button>
    </div>
  )
}

5.4. useReducer

stateactionにそれぞれ型を用意します。
actionはすべてのcaseにおけるtypepayloadの型をユニオン型で定義しておきます。

reducer関数の戻り値はstateと同じはずなので、型を明記しなくてよいかなと思います。

ex.3種類のボタンでcountを+1,-1,リセットする関数コンポーネント
Example.tsx
import { useReducer } from "react"

// 1. 状態の型
type CounterState = {
  count: number
}

// 2. アクションの型
type CounterAction =
  | { type: "increment" }
  | { type: "decrement" }
  | { type: "reset" }

// 3. reducer関数
const reducer = (state: CounterState, action: CounterAction) => { //戻り値はstateの型をもとに推論

  switch (action.type) {
    case "increment":
      return { count: state.count + 1 }
    case "decrement":
      return { count: state.count - 1 }
    case "reset":
      return { count: 0 }
    default:
      return state
  }
}

// 4. コンポーネント
const Counter = () => {
  const [state, dispatch] = useReducer(reducer, { count: 0 })

  return (
    <div>
      <p>カウント: {state.count}</p>
      <button onClick={() => dispatch({ type: "increment" })}>+1</button>
      <button onClick={() => dispatch({ type: "decrement" })}>-1</button>
      <button onClick={() => dispatch({ type: "reset" })}>リセット</button>
    </div>
  )
}

export default Counter;

5.5. Context API

state,context,propsの型を作成します。
ユーザーのログイン状態を管理するcontextを例に説明します。

1. stateの型定義
AuthUserにユーザーに持たせるプロパティの型をそれぞれ指定します。

ExampleContext.tsx
import { createContext, useContext, useState, ReactNode } from "react"

// ------------------------------
// 1. stateの型定義
// ------------------------------
type AuthUser = {
  id: string
  name: string
}

2. contextの型定義
このcontextのstateを他のコンポーネントでも型安全に使えるように、各stateの型を指定します。
user:先ほど作ったAuthUserと未ログインを表すnullをユニオン型で指定
login:引数の型はAuthUserであると指定。戻り値が無い関数なのでvoidを指定。
logout:引数も戻り値もない関数であると指定。

ExampleContext.tsx
// 2. contextの型定義
type AuthContextType = {
  user: AuthUser | null
  login: (user: AuthUser) => void
  logout: () => void
}

3. Context の作成(初期値はundefined)
steteと同じ形式で初期値を与える方法もありますが、エラーやバグに気づきにくいという問題があるので、より安全とされるundefinedを初期値にする方法で紹介します。

先ほど作成した、コンテキストで共有したいstateの型群(AuthContextType)とundefinedをユニオン型で定義します。
createContextには初期値を渡さないとエラーになるのでundefinedを与えました。
useContextを使うと、Providerが※マウントされていない場合は初期値、マウントされている場合は共有するstate群が返ります。
つまり、共有するstateとundefinedの両方を受け入れる型にしておく必要があるわけです。
これに加えて、5で紹介するエラーハンドリングを使うと安全性が上がります。

ExampleContext.tsx
// ------------------------------
// 3. Context の作成(初期値はundefined)
// ------------------------------
const AuthContext = createContext<AuthContextType | undefined>(undefined)

4. Provider コンポーネント

プロバイダーのchildrenにはコンポーネントが入ります。
コンポーネントの型はReactNodeです。

ExampleContext.tsx
// ------------------------------
// 4. Provider コンポーネント
// ------------------------------
type Props = {
  children: ReactNode
}

export const AuthProvider = ({ children }: Props) => {
  const [user, setUser] = useState<AuthUser | null>(null)

  const login = (newUser: AuthUser) => setUser(newUser)
  const logout = () => setUser(null)

  return (
    <AuthContext.Provider value={{ user, login, logout }}>
      {children}
    </AuthContext.Provider>
  )
}

5. カスタムフック
戻り値の型を記載しています。推論が効くので書かなくてもよいですが、何を返す関数なのかわかりやすいので私は書いてます。

マウントされていないコンポーネントでuseAuthが使われた場合は初期値のundefinedが返るので、if文内でエラーを出すようにすればミスに気づきやすくなります。

ExampleContext.tsx
// ------------------------------
// 5. カスタムフック
// ------------------------------
export const useAuth = (): AuthContextType  => {
  const context = useContext(AuthContext)
  if (context === undefined) {
    throw new Error("useAuthはAuthProviderの中で使用してください")
  }
  return context;
}

※マウント(使える状態にする)

Main.tsx
//共有stateを使いたいコンポーネントの親
<AuthProvider>          ← value={{ user, login, logout }} が設定される
  <Header />            ← useContext() → { user, login, logout }が使えるように
</AuthProvider>

<Header />                AuthProvider で囲まれてない
                         useContext()  undefined

5.6. reducerとcontext

2つ組み合わせて使う場合の注意点を本記事5.4の関数をcontextにしたものを使って簡単に解説します。

ex.ボタンで数値を増減、リセットするreducerを持つcontext
ExampleContext.tsx
import { createContext, useContext, useReducer, ReactNode } from "react";

// -------------------------
// 1. 状態とアクションの型
// -------------------------
type CounterState = { count: number };
type CounterAction =
  | { type: "increment" }
  | { type: "decrement" };

// -------------------------
// 2. reducer 関数
// -------------------------
const reducer = (state: CounterState, action: CounterAction): CounterState => {
  switch (action.type) {
    case "increment":
      return { count: state.count + 1 };
    case "decrement":
      return { count: state.count - 1 };
    default:
      return state;
  }
};

// -------------------------
// 3. Context の定義
// -------------------------
const CounterStateContext = createContext<CounterState | undefined>(undefined);
const CounterDispatchContext = createContext<React.Dispatch<CounterAction> | undefined>(undefined);

// -------------------------
// 4. Provider コンポーネント
// -------------------------
export const CounterProvider = ({ children }: { children: ReactNode }) => {
  const [state, dispatch] = useReducer(reducer, { count: 0 });

  return (
    <CounterStateContext.Provider value={state}>
      <CounterDispatchContext.Provider value={dispatch}>
        {children}
      </CounterDispatchContext.Provider>
    </CounterStateContext.Provider>
  );
};

// -------------------------
// 5. カスタムフック(state / dispatch)
// -------------------------
export const useCounterState = () => {
  const context = useContext(CounterStateContext);
  if (context === undefined) {
    throw new Error("useCounterStateはCounterProvider内で使ってください");
  }
  return context;
};

export const useCounterDispatch = () => {
  const context = useContext(CounterDispatchContext);
  if (context === undefined) {
    throw new Error("useCounterDispatchはCounterProvider内で使ってください");
  }
  return context;
};


jsの話ですが、dispatchを他のコンポーネントで使いたい場合は、stateとは別にdispatch用のcontextを作成します。dispatchの型はこのようになります。

const CounterDispatchContext 
= createContext<React.Dispatch<CounterAction> | undefined>(undefined);

これは 「この Context には CounterAction 型の dispatch 関数 または undefined が入ります」 という意味です。

6. イベント型付け

6.1 Reactの合成イベント

onChangeに対する型付けを例に解説します。
JSXのハンドラを渡す場合、Reactに型が用意されているのでそれを使います。
型のみを取得して使うには、importの後にtypeを付けます。

onChange用の型データをeに付けます。その後ろの<HTMLInputElement>ジェネリクスと言って、渡しておくとe.target.value を string として補完してくれます。

ex.入力欄の値を保存する関数コンポーネント
import { useState } from "react"
import type { ChangeEvent } from "react"

const TextInput = () => {
  const [text, setText] = useState("")

  const handleChange = (e: ChangeEvent<HTMLInputElement>) => {
    setText(e.target.value)
  }

  return <input type="text" value={text} onChange={handleChange} />
}

イベント型の例を列挙します。

イベント名 型例 解説
onChange ChangeEvent<HTMLInputElement> フォームの値変更
onClick MouseEvent<HTMLButtonElement> ボタンクリックなどのマウス操作
onSubmit FormEvent<HTMLFormElement> フォーム送信
onFocus FocusEvent<HTMLInputElement> フォーカスされたとき
onBlur FocusEvent<HTMLInputElement> フォーカスが外れたとき

6.2. ネイティブの DOM イベント

ex. ポップアップ領域外のクリックを検知する関数
import { useEffect, useRef } from "react"

const OutsideClickExample = () => {
  // ポップアップ要素への ref
  const popupRef = useRef<HTMLDivElement | null>(null)

  useEffect(() => {
    // ネイティブの MouseEvent を受け取るハンドラ
    const handleClickOutside = (e: MouseEvent) => {
      // ① e.target が Element(DOMノード)であるかチェック
      //    └ instanceof を使って「本当に要素か?」を判定
      if (!(e.target instanceof Element)) return

      // ② クリック対象が popupRef の中なら何もしない
      if (popupRef.current?.contains(e.target)) return

      // ③ それ以外――ポップアップ外をクリックしたら反応
      console.log("ポップアップの外がクリックされました")
    }

    // イベント登録/解除
    document.addEventListener("click", handleClickOutside)
    return () => {
      document.removeEventListener("click", handleClickOutside)
    }
  }, [])

  return (
    <div
      ref={popupRef}
      style={{
        width: 200,
        height: 100,
        background: "lightgray",
      }}
    >
      ポップアップ領域
    </div>
  )
}

export default OutsideClickExample

6.2.1 ネイティブのイベント型

ポップアップ領域外のクリックを検知する関数を例に解説します。

React の合成イベント (React.MouseEvent など) ではなく、
ブラウザ標準(ネイティブ)の MouseEvent 型を使います。

引数を (e: MouseEvent) と書くと、

  • e.clientXe.clientY
  • e.target
    などのプロパティが型安全に補完されます。

6.2.2 instanceof とは

instanceof は、
「このオブジェクトは指定したクラスのインスタンスですか?」
を調べる演算子です。

e.target の型は汎用的な EventTarget なので、
直接 contains()closest() を呼ぶと型エラーになります。
また、実行時に e.target がテキストノードや document の場合もあり得るため、
要素ノード以外では処理をスキップ して安全性を保つ必要があります。

if (!(e.target instanceof Element)) return

これで e.targetDOM の要素 であると保証した上で次の処理に移れます。

7. 非同期処理と API 呼び出し

7.1. async/await

ex.async/awaitの型
example.ts
type User = {
  id: number
  name: string
}

// ✅ ユーザーを取得する非同期関数
const fetchUser = async (): Promise<User> => {
  const res = await fetch("https://jsonplaceholder.typicode.com/users/1");
  const data: User = await res.json();
  return data;
};

// ✅ 呼び出し側
const main = async () => {
  const user = await fetchUser();
  console.log(user.name);
};

fetchUser関数の戻り値はUser型なので、

const fetchUser = (): User => {

今までの関数ならこうしていましたが、今回は非同期処理なので、

const fetchUser = async (): Promise<User> => {

Promiseの中に書きます。

また、fetchした生データには型情報がないので、何もしないとすぐに型エラーを吐きます。

const data: User = await res.json();

このようにして型情報を与えてあげましょう。

8. ジェネリクス活用例

8.1. 汎用的な関数コンポーネント

ジェネリック型を紹介します。
<T>は型の引数で、使うときに型を渡す機能があります。
これを使うと関数が型に縛られず、再利用性があがります。

8.1.1. 配列

ex.配列の先頭を取得する関数
function getFirstItem<T>(arr: T[]): T | undefined {
  return arr[0]
}

// 使用例
const numbers = [1, 2, 3]
const firstNumber = getFirstItem(numbers)  // 型: number

const names = ["Alice", "Bob"]
const firstName = getFirstItem(names)  // 型: string

8.1.2. オブジェクト

オブジェクトからキーを取得したい場合、以下のように書くと思いますが、Object.Key( )の型は常にstring[ ]を返すので型安全ではありません。実際は("id" | "name")[ ]というユニオン型の配列にしたい。

const user = { id: 1, name: "ほそかわ たかし" }
const keys = Object.keys(user) //型:string[]

そこで、以下のようなオブジェクトからキーを取得する関数を使うことで安全に型を取り出します。

ex.オブジェクトのキーを取得する関数
function getKeys<T extends object>(obj: T): (keyof T)[] {
  return Object.keys(obj) as (keyof T)[]
}

// 使用例
const user = { id: 1, name: "ほそかわ たかし" }
const keys = getKeys(user)  // 型: ("id" | "name")[]

ちょっと複雑なので表にしました。
部分 意味
<T extends object> Tは「オブジェクト型」だけ受け取れる
obj: T 引数objの型はT
keyof T T型の「キー名」だけを取り出す
as (keyof T)[] Object.keys() の戻り値を型変換して返す
  • getKeys関数は<T extends object>を書くことで、「T はオブジェクト型に限定される」ことを意味します。
    関数に「文字列」や「数値」などが渡されないよう、型制約をかけています。今回の場合、Tに渡ってくる型は関数の使用時に渡されたオブジェクトから推論して{ id: number; name: string }になります。
  • 引数 obj は型 T の値。つまり、関数を呼ぶときのオブジェクト型がそのまま型として使われます(型引数)
  • Object.keys(obj)でオブジェクトからキーだけ取り出します。
  • as (keyof T)[ ]と書いていますが、as型アサーションと言って、強制的に型を指定するものです。「Object.keys( ) の戻り値を keyof T[ ] だとみなしてください」という意味になります。
    例えば、T が { id: number; name: string } であれば、keyof T は ("id" | "name")[ ] になります。
  • return文でアサーションを行い、Object.keys( )の結果は(keyof T)[]だと指定したので、関数自体の戻り値も(keyof T)[]になります。

8.2. Utility Types

Partial<T>:すべてのプロパティを任意にする

ex.Partialの使い方
type User = {
  id: number;
  name: string;
  email: string;
};

const updateUser = (id: number, updates: Partial<User>) => {
  console.log(`ユーザー ${id} を次の内容で更新します:`, updates);
};

updateUser(1, { name: "あかり" });       // 名前だけ更新
updateUser(1, { email: "akari@example.com" }); // メールだけ更新
updateUser(1, {});                         // 何も更新しない(許可される)

上記のようにPartialの中に元々存在している型を渡すとその型の全項目がオプショナルになります。
stateを部分更新するときなどに使われます。

// updatesの型
type updates = {
  id?: number;
  name?: string;
  email?: string;
};

Pick<T, K>:特定のプロパティを抜き出す

ex.Pickの使い方
type User = {
  id: number;
  name: string;
  email: string;
};

type UserSummary = Pick<User, "id" | "name">;

const userCard: UserSummary = {
  id: 1,
  name: "あかり"
  // email は含まれていない
  
};
第一引数には型、第二引数にはキーを渡します。 型の中から指定したキーだけを含んだ新たな型が作成されるので、UserSummaryにはemailプロパティは含まれません。

Omit<T, K>:特定のプロパティを除外する

ex.Omitの使い方
type User = {
  id: number;
  name: string;
  email: string;
};

type UserWithoutId = Omit<User, "id">;

const newUser: UserWithoutId = {
  name: "あかり",
  email: "akari@example.com"
  // id は不要なので入れない
};
第一引数には型、第二引数にはキーを渡します。 Pickとは逆に、渡したキーが型から除外されます。 UserWithoutId にはidプロパティは含まれません。

9. 実践

以前、ポートフォリオとしてTodoアプリを作りました。

そちらのJavascriptをTypescriptに書き換えたので、GitHubを参考に書き換えの練習に使ってみてください。

10. まとめと今後の学習展望

学習の全体を通して、誰かに教えられるように理解することを心掛けました。
アウトプットを前提にすることで細部まで理解しようとするのでおすすめです。
ただ、理解できない時間が長いとストレスを感じてしまうので、ちょっと考えてわからなかったら他のことを学んで、時間をおいて読み返すなどしてみてください。案外スルッといけたりします。

新たに学んだことは追記していく予定です。
また、間違い等ありましたらコメントいただけると幸いです。

お疲れさまでした!一緒に頑張りましょう!

2
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?