iimon TECH BLOG

iimonエンジニアが得られた経験や知識を共有して世の中をイイモンにしていくためのブログです

TypeScriptのUtilityTypesについて調べてみた

■はじめに

こんにちは、株式会社iimonでフロントエンドエンジニアをしている「やーさん」です。

本記事はアドベントカレンダー12日目の記事になります!

業務の中でTypeScriptを利用するのですが、TypeScriptに用意されているユーティリティー型を使う機会がなかったので、どんな物があるのか調べてみました。

■ユーティリティ型とは?

TypeScriptのユーティリティ型は、既存の型を変換して新しい型を作成するための組み込みの型ツールです。

ユーティリティ型を使うことで、型の定義を効率的に行うことができます。

Documentation - Utility Types

■Partial< Type >型

◆説明

Partial<Type>に渡されたオブエジェクトのすべてのプロパティを optional に設定した型を作ります。

type User = {
    name: string;
    email: string;
    age: number;
};

type PartialUserType = Partial<User>

User型の各プロパティに | undefinedが付与されていることがわかります。

このように、オプショナルなプロパティを簡単に作成できます。

◆どんなときに使えそう?

type Article = {
    id: string;
    title: string;
    description: string;
};

const updateArticle = (article: Article, inputData: Partial<Article>) => {
    return {
        ...article,
        ...inputData,
    };
};

const article = { id: '1', title: 'タイトル01', description: '説明01' };
const inputData = { title: 'タイトル02' };

ユーザーが記事を更新するケースを考えてみます。

変数articleには、データベースに保存されている記事の内容がオブジェクトで代入されています。

変数inputDataには、ユーザーが更新した情報がオブジェクトで代入されています。

更新した記事をデータベースにPOSTするため、変数articleを変数inputDataで更新するupdateArticle関数を定義しています。

updateArticleの第2引数は、Article型の中でどれかがオプショナルで入ってくるので、Partial<Article>でオプショナルに変換して、Article型のオプショナルでどれが入ってきても柔軟に受け入れるようにできます。

■Required< Type > 型

◆説明

type Article = {
    id: string;
    title?: string;
    description?: string;
};

type requiredArticleType = Required<Article>;

Required<Type>は、<Type>でオブジェクトを受け取って、全てのプロパティを必須にします。

Article型のtitleやdescriptionはオプショナルになっていますが、Required<Article>とすることで、オプショナルを削除した型を作ることができます。

◆どんなときに使えそう?

interface FormData {
    userName?: string;
    password?: string;
    confirmPassword?: string;
}

const validation = (data: FormData) => {
    // バリデーションチェック
}

const submitForm = (data: Required<FormData>) => {
    // すべてのフィールドが存在することが保証される
    if (data.password !== data.confirmPassword) {
        throw new Error('Passwords do not match');
    }
    // 送信処理
}

ユーザーがformに入力するアプリで、入力しない場合もあり得るため、FormDataはオプショナルで定義しています。

バリデーションチェックするvalidation関数の引数もFormData型にしています。

しかし、送信するときは必ず存在すると仮定すると、送信するsubmitForm関数の引数は、FormData型のオプショナルを削除した型で良いです。

そのため、Required<FormData>でオプショナルを削除した型を作ることが簡単にできます。

■Readonly< Type >型

◆説明

type Article = {
    id: string;
    title: string;
    description: string;
};

type ReadonlyArticleType = Readonly<Article>;

Readonly型は、でオブジェクト型を受け入れ、すべてのプロパティをreadonlyとします。

Article型のプロパティが全てreadonlyになっていることがわかります。

◆どんなときに使えそう?

type Article = {
    id: string;
    title: string;
    description: string;
};

const article = { id: '1', title: 'タイトル', description: '説明' };

const submit = (article: Readonly<Article>) => {
    article.title = 'タイトルの更新';
};

submit関数で、送信する場合に引数で受け取ったデータを書き換えさせたくないと仮定します。

その場合、submit関数の引数の型をReadonly<Article>とすることで、関数内でプロパティを書き換えられないようにできます。

このように関数の引数で受け取ったオブジェクトを書き換えられたくない場合に有用な気がしました。

■Record< Keys, Type >型

◆説明

interface User {
    id: string;
    name: string;
    age: number;
}

type UserMap = Record<User['id'], User>;

const users: UserMap = {
    '1': { id: '1', name: 'John', age: 30 },
    '2': { id: '1', name: 'Jane', age: 25 },
};

プロパティキー(Keys)と、それらのキーに関連付けられる型(Type)を持つオブジェクト型を生成するためのユーティリティ型です。

User型のidをキーに、値をUser型にした新たな型を簡単につくれます。

◆どんなときに使えそう?

type ApiEndpoints = Record<
    string,
    {
        method: 'GET' | 'POST' | 'PUT' | 'DELETE';
        path: string;
    }
>;

const endpoints: ApiEndpoints = {
    getUser: {
        method: 'GET',
        path: '/users/:id',
    },
    createUser: {
        method: 'POST',
        path: '/users',
    },
};

APIのエンドポイントを、役割ごとにオブジェクトで定義する場合を考えてみます。

  • オブジェクトのkeyがString型
  • methodがUnion型
  • pathがstring型

の場合、Record<Keys, Type>に上記のような型を渡すことで、オブジェクトのプロパティと値の型を柔軟に定義できるようになります。

■Pick< Type, Keys >型

◆説明

interface User {
    id: number;
    name: string;
    email: string;
    age: number;
    address: string;
}

type UserBasicInfo = Pick<User, 'name' | 'email'>;

Typeに渡したオブジェクトから、プロパティKeysを選んだ型を構築します。

上記の例では、User型から、nameとemailのUnion型を新たに作ることができます。

◆どんなときに使えそう?

interface Product {
    id: number;
    name: string;
    price: number;
    description: string;
}

function updateProduct(id: number, updates: Pick<Product, 'name' | 'price' | 'description'>;) {
    // id以外の指定されたフィールドのみ更新可能
}

updateProduct(1, {
    name: '新しい商品名',
    price: 1000,
    description: '商品の説明',
});

商品情報を更新する関数updateProductがあります。

updateProduct関数では、更新可能なデータのみ受け取れるようにするために、Pick<Type, Keys>を使っています。

こうすることで、更新可能なデータ(name、price、descriptionのオブジェクト型)のみ受け取ることができる型を作成できます。

■Omit< Type, Keys >型

◆説明

interface User {
    id: number;
    name: string;
    email: string;
    age: number;
    address: string;
}

type UserBasicInfo = Omit<User, 'name' | 'email'>;

Omit<Type, Keys>は、TypeオブジェクトからKeysを削除した新しい型を作ります。

なので、Pick<Type, Keys>の逆のことをします。

上記画像では、User型から「name」と「email」を省いたからができていることがわかります。

◆どんなときに使えそう?

interface DatabaseUser {
    id: number;
    firstName: string;
    lastName: string;
    email: string;
    passwordHash: string;
    role: string;
    lastLogin: Date;
}

type UserProfile = Omit<DatabaseUser, 'id' | 'passwordHash' | 'lastLogin'>;

const createUserProfile = (user: DatabaseUser): UserProfile => {
    return {
        firstName: user.firstName,
        lastName: user.lastName,
        email: user.email,
            role: user.role
    };
}

データベースから取得したDatabaseUser型が、createUserProfile関数の引数に入ってくると仮定します。

DatabaseUser型の中から、プロフィールとして表示するのは「firstName」「lastName」「email」「role」だけだとします。

その場合、createUserProfile関数は、「firstName」「lastName」「email」「role」のオブジェクトを作成して返せばいいので、

Omit<DatabaseUser, 'id' | 'passwordHash' | 'lastLogin'>;で不要なものを削除した型を作ることができます。

■Exclude< UnionType, ExcludedMembers >型とExtract< Type, Union >の型

◆説明

type Numbers = 1 | 2 | 3 | 4 | 5;
type EvenNumbers = Exclude<Numbers, 1 | 3 | 5>;

Excludeは、UnionTypeを受け取って、UnionTypeから削除した型のユニオンを作ります。

例えば、数値のUnion型から、偶数のみを抽出することができます。

type Numbers = 1 | 2 | 3 | 4 | 5;
type OddNumbers = Extract<Numbers, 1 | 3 | 5>;

Extractは、Excludeの逆で、TypeUnion型から、Union型のみを抽出した型を作ります。

◆どんなときに使えそう?

type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH' | 'HEAD' | 'OPTIONS';
type WriteHttpMethod = Exclude<HttpMethod, 'GET' | 'HEAD' | 'OPTIONS'>;

function makeWriteRequest(method: WriteHttpMethod, url: string) {
    // POST, PUT, DELETE, PATCH のみ許可
}

HTTPリクエストを送るメソッドで、特定のメソッドのみ引数で受け取れるようにする場合などに使えそうな気がしました。

■NonNullable< Type >型

◆説明

type NonNullableType = NonNullable<string | number | undefined | null>;

NonNullable型は、ユニオン型を受け入れ、nullやundefinedを含まない新しい型を返します。

◆どんなときに使えそう?

// フォーム入力の型定義
type FormInputType = {
    username: string | undefined;
    email: string | null;
    age: number | undefined;
};

// バリデーション済みデータの型定義
type ValidatedFormType = {
    [K in keyof FormInputType]: NonNullable<FormInputType[K]>;
};

class FormProcessor {
    validateForm(input: FormInputType): ValidatedFormType {
        // 必須入力チェックのチェック
        if (!input.username) {
            throw new Error('ユーザー名を入力してください');
        }

        if (!input.email) {
            throw new Error('メールアドレスを入力してください');
        }
        
        if (!input.age) {
         throw new Error('年齢を入力してください');
        }

        return {
            username: input.username,
            email: input.email,
            age: input.age,
        };
    }
}

ユーザーがformに入力した型がFormInputType型と仮定します。

バリデーションチェックした後の型では「null」や「undefined」を除外できるため、

NonNullable<Type>で「null」や「undefined」を除外した新たな型を簡単に作れそうです。

■ReturnType< Type >型

◆説明

const add = (a: number, b: number) => a + b;

type AddResultType = ReturnType<typeof add>;

ReturnType<Type>は、関数の戻り値から型を作る事ができます。

add関数は、number型を返すため、ReturnType<typeof add>;でnumber型を作ることができます。

◆どんなときに使えそう?

const getApiResponse = () => ({
    data: { items: [1, 2, 3] },
    status: 200,
});

type ApiResponse = ReturnType<typeof getApiResponse>;

const transFormApiResponse = (response: ApiResponse) => {
    // APIレスポンスのデータを変換する
}

const responce = getApiResponse();

transFormApiResponse(responce);

APIにリクエストを投げて、getApiResponse関数の戻り値の型をtransFormApiResponse関数の引数に渡したいと仮定します。

その場合、getApiResponseの戻り値型をtransFormApiResponse関数の引数に指定したいですが、typeなどを使って明示的に型を定義するのは面倒です。

その場合、ReturnType<Type>の関数名を指定することで、関数の戻り値の型を簡単に作成できます。

■まとめ

TypeScriptのユーティリティー型を使えば、既存の定義されている型を使いながら、より絞り込んだ柔軟な型を作れることがわかりました。 型が抽象的な場合より、具体的な方がコードの把握や、予期しないエラーをビルド時に検知できるため、良いかなと思いました。

■最後に

現在弊社ではエンジニアを募集しています!

この記事を読んで少しでも興味を持ってくださった方は、ぜひカジュアル面談でお話ししましょう!

iimon採用サイト / Wantedly / Green

最後まで読んでいただきありがとうございました!

次のアドベントカレンダーの記事はかとうさんです! どんな記事を書いてくれるのか楽しみですね!

■参考記事

TypeScript: Documentation - Utility Types

【TypeScript】Utility Typesをまとめて理解する #TypeScript - Qiita

TypeScript のユーティリティ型全部