1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

[TypeScript] ジェネリックをマスターしよう!

Last updated at Posted at 2024-05-04

概要

TypeScriptのジェネリックについてまとめました。

【目次】

まずは基本的な型宣言をマスターしよう!
オブジェクト型の型宣言をマスターしよう!
関数の型定義をマスターしよう!
配列をマスターしよう!
インターフェースをマスターしよう!
クラスを使いこなそう!
型修飾子についての理解を深めよう!
ジェネリックをマスターしよう! ←🈁
独自の構文拡張をマスターしよう!

--

ジェネリックとは?

ジェネリックとは 実際に利用されるまで型が確定しない抽象的なデータ型 です。
以下のように宣言します。

  • <T>

ジェネリックの命名規則

一般的な命名規則についてご紹介します。

命名 利用ケース
T 標準的な命名
type, template を表す
U, V 複数パラメータがある場合に使用する
S 状態管理のライブラリ
stateを表す
K, V データ構造を利用する場合に用いられる
Kkey, Vvalueを指す

[注意点]
コンテキストでの意図が伝わりにくい場合は、より具体的な命名をすること。

これらを踏まえて実際の実装を見ていきましょう。


ジェネリック関数

ジェネリック関数は以下のように定義します。

function genericFunc<T>(input: T) {
    return input;
}

let typeString = genericFunc("string");
let typeNumber = genericFunc(1234);

ジェネリック関数を使用することで任意の型を引数に取る関数を作成できます。

:pencil: [注意点]アロー関数

アロー関数を使用すると、JSXの構文と競合するのでエラーが発生します。

// NG
let genericAllow<T> = (input: T) => input;

明示的な型指定

ジェネリックは明示的に型(パラメータ)を指定することが出来ます。

ジェネリックインターフェースの利用、ジェネリッククラス継承時に利用します。

function genericFunc<T>(input: T) {
    return input;
}

let typeString = genericFunc<string>("string");
let typeNumber = genericFunc<number>(1234);

ジェネリックインターフェース

ジェネリックはインターフェースに対しても指定することが可能です。
インターフェース利用時に明示的に型指定する必要があります。

interface genericInterface<T> {
    prop: T
}

let hoge: genericInterface<string> = {
    prop: "abc"
}

:pencil: [参考] Arrayメソッド

Arrayメソッドはジェネリックインターフェースとして定義されています。

interface Array<T> {
    pop(): T | undefined;

    push(...items: T[]): number
}

ジェネリッククラス

クラスに対しても、ジェネリックを指定することが可能です。

class GenericKlass<T> {
    prop: T

    constructor(prop: T){
        this.prop = prop;
    }
}

クラスの拡張

ジェネリッククラスをベースクラスとしたクラスの拡張も可能です。
デフォルトの型を持たないパラメータに対しては明示的な型指定が必要です。

class GenericKlass<T> {
    prop: T

    constructor(prop: T){
        this.prop = prop;
    }
}

class SubKlass extends GenericKlass<string> {}

サブクラスもジェネリッククラスとし、型パラメータをベースクラスに対しても指定することも可能です。

class GenericKlass<T> {
    prop: T

    constructor(prop: T) {
        this.prop = prop;
    }
}

class SubKlass<T, U> extends GenericKlass<T> {
    types: U

    constructor(prop: T, types: U) {
        super(prop);
        this.types = types
    }
}

ジェネリックインターフェースの実装

ジェネリックインターフェースの実装は、以下のように行います。

interface SampleInterface<T> {
    value: T
}

class Klass<T> implements SampleInterface<T> {
    value: T;

    constructor(value: T) {
        this.value = value;
    }
}

ジェネリックメソッド

クラスメソッドは独自のジェネリック型を定義することが可能です。
クラスメソッドの呼び出しでは、呼び出しごとに異なるパラメータを指定することが出来ます。

class Klass<T> {
    value: T;

    constructor(value: T) {
        this.value = value;
    }

    genericMethod<U>(input: U) {
        return {value: this.value, key: input}
    }
}

let klass = new Klass("abc");
klass.genericMethod(1234);
klass.genericMethod("def");

静的メンバーとジェネリック

静的メンバーもジェネリックを利用できますが、
クラスに紐づいているため、インスタンス固有のパラメータのアクセスはできません。

アクセスした場合、以下のようにエラーが発生します。

class Klass<T> {
    value: T;

    constructor(value: T) {
        this.value = value;
    }

    static staticMethod<V>(input: V) {
        //NG: Static members cannot reference class type parameters.
        let tmp: T;
    }
}

ジェネリック型エイリアス

型エイリアスもジェネリックを利用することが出来ます。

type SampleAlias<Input, Output> = (input: Input) => Output
let inputStringOutputNumber: SampleAlias<string, number>;

// OK
inputStringOutputNumber = (input: string) => input.length;

// NG: Type '(input: string) => () => string' is not assignable to type 'SampleAlias<string, number>'.
inputStringOutputNumber = (input: string) => input.toUpperCase();

:pencil: [デザインパターン] ジェネリック + タグ付き合併型

ジェネリック型とタグ付き合併型を組み合わせることで、再利用可能な型をモデル化することが出来ます。

サンプルコードでは、型が異なるdataを格納するログデータをモデル化しています。
これにより、処理を再利用することができます。

interface SuucessLog<T> {
    data: T,
    type: "success"
}

interface FailreLog<T> {
    data: T,
    type: "fail"    
}

type Log<T> =  SuucessLog<T> | FailreLog<T>

function checkLog<T>(log: Log<T>) {
    if(log.type === "success") {
        console.log("successed!");
        console.log(`data is ${log.data}`);
    } else {
        console.log("failed!");
        console.log(`data is ${log.data}`);
    }
}

let logFirst: Log<string> = {
    data: "process1 OK",
    type: "success"
}

let logSecond: Log<string> = {
    data: "process2 OK",
    type: "success"
}

let logThird: Log<number> = {
    data: 19389328103941,
    type: "fail"
}

checkLog(logFirst);
checkLog(logSecond);
checkLog(logThird);

ジェネリックのデフォルト値

型パラメータにはデフォルトの型を指定することが可能です。
<T = デフォルト型>

ジェネリックインターフェースの利用, クラス継承時の明示的な型指定を、デフォルト値を用いることで省略できます。

interface genericInterface<T = string> {
    prop: T
}

let hoge: genericInterface = {
    prop: "abc"
}

制限付きジェネリック

ジェネリック型にはどんな値も渡すことが可能です。
しかし、特定のセットの方だけを扱うことを意図したい場合もあるでしょう。

extendsキーワードを指定することで、型パラメータがある型を拡張する必要があると宣言することができます。

以下のサンプルコードでは、
CallLength関数に渡される引数がWithLengthに割当可能であることを制限しています。

interface WithLength {
    length: number
}

function CallLength<T extends WithLength>(input: T){
    input.length
}

CallLength("call CallLength");
CallLength([1234, 4567]);

// NG: Argument of type 'Date' is not assignable to parameter of type 'WithLength'.
CallLength(new Date());

extends WithLengthの指定がない場合を見ていきましょう。

以下のエラーが発生します。
渡された方パラメータがlegnthを保持していない可能性を示唆していますね。

interface WithLength {
    length: number
}

function CallLength<T>(input: T){
    // NG: Property 'length' does not exist on type 'T'.
    input.length
}

:pencil: [応用] 制限付きジェネリック + keyof 演算子

制限付きジェネリックと、keyof演算子を連携することで、よい開発者体験を得ることが出来ます。

サンプルコードではKey extends keyof Tを利用して、関数getの引数を制限しています。
T[Key]が返るので、戻り値の型はstring or number となります。

function get<T, Key extends keyof T>(obj: T, key: Key) {
    return obj[key]
}

interface Person{ 
    name: string,
    age: number
}

let taro: Person = {
    name: "taro",
    age: 30
}

// let taroName: string
let taroName = get(taro, "name");
// let taroAge: number
let taroAge = get(taro, "age");

[参考]
keyof 演算子を利用した場合は以下の挙動になります。

T[keyof T]のデータが返るので、戻り値がより抽象的なstring | number型となります。

function get<T>(obj: T, key: keyof T) {
    return obj[key]
}

interface Person{ 
    name: string,
    age: number
}

let taro: Person = {
    name: "taro",
    age: 30
}

// let taroName: string | number
let taroName = get(taro, "name");
// let taroAge: string | number
let taroAge = get(taro, "age");

:pencil: [参考] Promise

TypeScriptでは、Promiseはジェネリッククラスとして宣言されています。
このパラメータには、Promiseが成功された(fulfilled)ときに返す値の型を指定します。

明示的な型指定

型指定した場合のサンプルコードはこちらです。
指定された型と、resolveの引数型が一致しない場合は型エラーが発生します。

const unknownPromise = new Promise((resolve) => {
    setTimeout(() => {
        console.log("1seconds");
        resolve("done");
    }, 1000);
});

const stringPromise = new Promise<string>((resolve) => {
    setTimeout(() => {
        console.log("1seconds");
        resolve("done");
    }, 1000);
});

const numberPromise = new Promise<number>((resolve) => {
    setTimeout(() => {
        console.log("1seconds");
        resolve(1234);
    }, 1000);
});

型指定がない場合

明示的に型を指定しない場合、パラメータの型が unknownであることを推論します。
以下のサンプルコードでは、型指定がないことが原因でエラーが発生しています。

function resolveString() {
    return new Promise<string>((resolve) => setTimeout(()=> resolve("done"), 1000))
}     

function resolveNumber() {
    return new Promise((resolve) => setTimeout(()=> resolve(12.34), 1000))
}

// NG: 'result' is of type 'unknown'.
function asyncProcess() {
    resolveString().then(resolveNumber).then(result => console.log(result.toFixed()));
}

:pencil: [参考] Promiseの実践コード

import fs from 'fs/promises';

/**
 * ファイルを読込
 * @param {string} filePath - 読み込むファイルのパス
 * @returns {Promise<string>} - 読み込まれたファイルの内容を表すPromise
 */
function readFile(filePath: string): Promise<string> {
    return fs.readFile(filePath, 'utf8');
}

/**
 * データ加工
 * @param {string} data - 加工するデータ
 * @returns {Promise<string>} - 加工されたデータを表すPromise
 */
function processData(data: string): Promise<string> {
    return new Promise((resolve) => {
        const processedData = data.toUpperCase();
        resolve(processedData);
    });
}

/**
 * APIリクエスト
 * @param {string} data - 送信するデータ
 * @returns {Promise<Response>} - APIからのレスポンスを表すPromise
 */
function sendData(data: string): Promise<Response> {
    return fetch('https://example.com/api/data', {
        method: 'POST',
        body: JSON.stringify({ data }),
        headers: { 'Content-Type': 'application/json' }
    });
}

// 実行
readFile('path/to/file.txt')
    .then(processData)
    .then(sendData)
    .then(response => response.json())
    .then(json => console.log('Response from API:', json))
    .catch(error => console.error('Error:', error));

まとめ

以上です!
ジェネリックを利用するのはC#以来でしたので、楽しみながら学習できました。

1
1
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
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?