🏄‍♂️

TypeScriptのGenericsをざっくり理解する

2022/09/06に公開

はじめに

株式会社Another worksでエンジニアインターンをしているhakkeiと申します。
主にTypescript, RectNativeでアプリの開発をしています。
「Genericsって結局どこで使うの?」といったテーマで社内LT会に参加したので記事にまとめました。

Genericsとは

genericとは下記の意味
英和辞典によると
①一般的な,包括的な
②総称の

TypeScriptのdocumentに公式的な定義はなかったが、Typescriptにおけるgenericとは型を抽象化すること、「総称型」と一般的に言われている。

https://www.typescriptlang.org/docs/handbook/2/generics.html

Javaの公式documentによると
「一言で言えば、ジェネリックスは、クラスやインターフェース、メソッドを定義する際に、型(クラスやインターフェース)をパラメータとして使用できるようにするものです。」

ジェネリックプログラミング
wikipediaによると、ジェネリックプログラミングとは、特定のデータ型に依存しないアルゴリズムを記述するためのプログラミングスタイル
データ型の詳細化を後回しにする方針によって、アルゴリズムが扱うデータ型の詳細は、そのインスタンス化の時に与えられる型パラメータで決定される

つまり、型をパラメータとして外部から渡せるようにし、抽象化したもの。

実際のコードを見てみる

まずはGenericsを使用しないパターン
受け取ったnumberやstring型のvalueをそのまま返す関数。
引数に指定した型以外を入れるとコンパイルエラーになる。

export const  returnNumber = (num: number) => {
  return num
}
export const  returnString = (str: string) => {
  return str
}

const num = returnNumber(25)
// 25
const str =  returnString("hakkei")
// "hakkei"

Genericsを使用すると...

export const returnValue = <T>(value: T) => {
  return value
}

const num = returnValue<number>(25)
// 25
const str =  returnValue<string>("hakkei")
// ”hakkei"

const num = returnValue<number>("hoge")
// number型にstring型を入れているのでコンパイルエラーをはいてくれる

先の2つの関数を抽象化して一つの汎用的な関数を作成できた!!

Utility型でGenericsを理解する

TypeScriptは、一般的な型変換を容易にするためにいくつかのUtility型を提供しています。(Typescript公式)
簡単に言うとTypeScriptが提供する便利な型

Utility型には内部でGenericsが使用されています。
例をいくつか紹介

Partial<T>

型Tのすべてのプロパティを省略可能(optional)にした新しい型を返す

Pick<T, K>

既に存在する型Tの中からKで選択した一部のプロパティのみを含んだ新たな型を構築します。

type User = {
  id: number;
  name: string,
  job: string
}

// 下記のtypeに変換される
// type PartialUser = {
//   id?: number;
//   name?: string,
//   job?: string
// }
type PartialUser = Partial<User>

// idとnameプロパティがpickされ下記に変換
// type PickUser = {
//   id: number;
//   name: string
// }
type PickUser = Pick<User, "id" | "name">

ついでにPartial型の中身を見てみると、、、

type Partial<T> = {
    [P in keyof T]?: T[P];
};

MappedTypeが使用されており、Userをmapして取り出したkeyすべてに「?」をつけてoptionalにしている。
(MappedTypeに関してはこちらがわかりやすかったです)

Partialを使用せずに表すと以下のように表せる。

type PickUser =  {
  [P in "id" | "name"]?: User[P];
};

応用編

genericsを使用して抽象化した関数

参考
https://blog.mitsuruog.info/2019/03/try-typescript-generics-101

// ex) オブジェクに要素をマージする関数
const  merge = <T extends { id: number }>(array: T[], newValue: T) => {
  // 変更するitemのidを探す
  const index = array.findIndex(item => item.id === newValue.id);

  // 見つからない場合は-1を返す
  if (index === -1) {
    return [
        ...array,
        newValue,
    ];
  } else {
    return [
      // 順番を変えないようにsliceを使用
        ...array.slice(0, index),
        newValue,
        ...array.slice(index + 1),
    ];
  }
}

// extends { id: number }のため{ id: number }を含んだオブジェクトの配列であればOK
const users: User[] = [
  { id: 1, name: 'Kota', age: 19 },
  { id: 2, name: 'Hake' ,age: 20 },
  { id: 3, name: 'Miya',age: 23 },
  { id: 4, name: 'Moya', age: 26 },
  { id: 5, name: 'Masa', age: 28 },
];

const mergedArray = merge<User>(users, {id: 2, name: "Jump", age: 22})
// →id=2のuser.nameがJumpになる

Genericsを使用した汎用的なセレクターコンポーネント

valueの型をGenericsで呼び出し側から指定できる。

import React, { useMemo, useState } from "react";

export type Option<T> = {
  label: string;
  key: string;
  value: T;
};

type Props<T> = {
  options: Option<T>[];
  onOptionClick: (value: T) => void;
  value: T;
};

function Selector<T>({ options, value, onOptionClick }: Props<T>) {
  const [optionVisible, setOptionVisible] = useState(false);
  
  const bottonText = useMemo(() => {
    const label = options.find((option) => option.value === value)?.label;
    return label ? label : "ボタンです";
  }, [options, value]);
  
  return (
    <div>
      <button
        onClick={() => {
          setOptionVisible(!optionVisible);
        }}
      >
        {bottonText}
      </button>
      {optionVisible &&
        options.map((option) => (
          <div
            key={option.key}
            onClick={() => onOptionClick(option.value)}
          >
            <text>{option.label}</text>
          </div>
        ))}
    </div>
  );
}

export default Selector;

呼び出し側


import React, { useState } from "react";
import Selector, { Option } from "../components/1025/Selector";
import Layout from "../components/Layout";

// valueがstring型のoptions
const stringOptions: Option<string>[] = [
  {
    label: "イヌ",
    key: "inu",
    value: "イヌ",
  },
  {
    label: "サル",
    key: "saru",
    value: "サル",
  },
  {
    label: "キジ",
    key: "kiji",
    value: "キジ",
  },
];

// valueがnumber型のoptions
const numberOptions: Option<number>[] = [
  {
    label: "10",
    key: "10",
    value: 10,
  },
  {
    label: "200",
    key: "200",
    value: 200,
  },
  {
    label: "3000",
    key: "3000",
    value: 3000,
  },
];

const GenericsPage = () => {
  const [selectedString, setSelectedString] = useState<string>("");
  const [selectedNumber, setSelectedNumber] = useState<number>(0);
  
  return (
    <div>
        {/* stringのselector */}
        <Selector<string>
          options={stringOptions}
          {/* 引数のvalueはstring */}
          onOptionClick={(value) => setSelectedString(value)}
          value={selectedString}
        />
        {/* numberのselector */}
        <Selector<number>
          options={numberOptions}
          {/* 引数のvalueはnumber */}
          onOptionClick={(value) => setSelectedNumber(value)}
          value={selectedNumber}
        />
    </div>
  );
};

export default GenericsPage;


最後に

より学びたい人は下記のリンクにtypeの問題がたくさんあるのでやってみてください!
https://github.com/type-challenges/type-challenges

▼複業でスキルを活かしてみませんか?複業クラウドの登録はこちら!
https://talent.aw-anotherworks.com/?login_type=none

Discussion