🐈

[Typescript]便利なUtility Types

2021/12/18に公開

はじめに

  • こんにちは。普段はReact×Typescriptで開発をしているmzdです。
  • 最近「開発の中で似たような型を定義することが多いな。」とよく感じていて、もっとスマートに書けたらかっこいいし変更に強い型を作れるんじゃないかなと思ったので、UtilityTypesを一通り調べました。
  • その中で便利で良いものが沢山あったので一部紹介していきます。
  • また使い方や新たにどういう型を作れるかだけでなく、UtilityTypesがTypescriptにおいて、どのように実装されているかを調べることでTypescript自体の理解も深めることができたので、そこにも触れていきます。

目次

  • UtilityTypesとは
  • Partial<T>
  • Required<T>
  • Readonly<T>
  • Record<K, T>
  • Pick<T, K>
  • Exclude<T,U>
  • Extract<T,U>
  • Omit<T, K>

UtilityTypesとは

公式ドキュメントには下記のように書かれています。

一般的な型変換を容易にするためのいくつかのユーティリティ型

  • この言葉の通りUtilityTypesは既に作成された型から「型変換をする」 ≒ 「新しい型を作成する」便利な型です。
  • これらはTypeという名からわかる通り、Typescript内でtype Partial<T> = のように型として定義されています。
  • そして、この<T>に既に作成された型を渡すことで、新しい型を簡単に作ることができ、私たちは変更に強い型を定義することやコードの冗長化を防ぐことができます。

例として使用する型

  • ここから、いくつかのUtilityTypesについて例を参考に見ていきます。
  • その時に既に定義された型として下記のCharacterTypeを基本的に使いますが、この型だけでは説明できない場合はその都度、型を定義します。
  • ( HUNTER × HUNTERが大好きなので、そのキャラクター名が出てきますが、あまり気にしないでください。)
interface CharacterType {
  id: number;
  name: string;
  age: number;
}

Partial<T>

  • Partial<T>は、Tの全てのプロパティをOptional(任意)にします。
type CharacterPartialType = Partial<CharacterType>

const characterPartial :CharacterPartialType = {
  name: "ゴン=フリークス";
}

# CharacterTypeであればid, ageも必要だがCharacterPartialTypeはPartial<T>によってOptionalになったので
characterPartialはid, ageがオブジェクトに無くても型エラーは発生しません。

TypescriptのPartial<T>の定義

type Partial<T> = {
    [P in keyof T]?: T[P];
};
  • P in keyof T
    • Tのkeyを一つずつ取り出してPに再定義しています。
    • 今回の例で言うとPはid, name, ageの3つになります。
  • T[P]
    • TPというkeyの型を意味します。
    • 今回のPはid, name, ageなので、この3つそれぞれの型(number, string, number)になります。
  • ?
    • ?をkeyの:の直前に付与することでOptional(任意)にしています。
  • このようにしてPartial<T>で受け取ったTから型を再作成して、keyに対して?をつけることで、全てのプロパティをOptionalのプロパティにしています。

Required<T>

  • Required<T>は、Partial<T>とは反対で、Tの全てのプロパティをRequired(必須)にする。
# 全てOptionalのCharacterType
interface CharacterType {
  id?: number;
  name?: string;
  age?: number;
}

type CharacterRequiredType = Required<CharacterType>

const characterRequired :CharacterRequiredType = {
  name: "キルア=ゾルディック" # idとageが不足
}

# CharacterRequiredTypeはCharacterTypeのプロパティを必須にした型なので
上記のコードだとidとageが不足しているため下記の型エラーが出ます。
	
# 型 '{ name: string; }' には 型 'Required<CharacterType>' から
次のプロパティがありません: id, age ts(2739)

TypescriptのRequiredの定義

type Required<T> = {
    [P in keyof T]-?: T[P];
};
  • P in keyof T

    • Tのkeyを一つずつ取り出してPに再定義しています。
    • 今回の例で言うとPはid, name, ageの3つになります。
  • T[P]

    • TPというkeyの型を意味します。
    • 今回のPはid, name, ageなので、この3つそれぞれの型(number, string, number)になります。
  • -?

    • ?はOptional(任意)を表すもので、?Pのそれぞれから除くことで必須に変更しています。

Readonly<T>

  • Readonly<T>は、Tの全てのプロパティをReadonlyにします。
  • Readonlyになったプロパティは再代入不可となり変更できないようになります。
type CharacterReadOnlyType = Readonly<CharacterType>

const CharacterReadOnly: CharacterReadOnlyType = {
  id: 403,
  name: "クラピカ",
  age: 17
}

CharacterReadOnly.id = 99  // エラー

# CharacterReadOnlyTypeは`Readonly`のため、再代入しようとするとエラーになります。

TypescriptのReadonly<T>の定義

type Readonly<T> = {
    readonly [P in keyof T]: T[P];
};
  • [P in keyof T]
    • Tのkeyを一つずつ取り出してPに再定義しています。
    • 今回の例で言うとPはid, name, ageの3つになります。
  • T[P]
    • TPというkeyの型を意味します。
    • 今回のPはid, name, ageなので、この3つそれぞれの型(number, string, number)になります。
  • readonly
    • 各プロパティの直前にreadonlyを付与することでその型を再代入不可にしています。
    • Readonly<T>を使用した場合、全てのプロパティが再代入不可になりますが、ある特定のプロパティのみ再代入不可にしたい場合は、そのプロパティにのみreadonlyを付与することで実現できます。

Record<K, T>

  • Record<K, T>は、KをプロパティとしてTを持つ型を作成します。
type CharacterRecordType = Record<number, CharacterType>

const CharacterRecord: CharacterRecordType = {
  99: {
    id: 99,
    name: "キルア=ゾルディック",
    age: 12
  },
  2: {
    id: 403,
    name: "レオリオ=パラディナイト",
    age: 19
  }
}

# K => number, T => CharacterTypeなので、numberをプロパティとしてCharacterTypeを持つ型を作成します。

TypescriptのRecord<K,T>の定義

type Record<K extends keyof any, T> = {
    [P in K]: T;
};
  • P in K
    • Kの値を一つずつ取り出してPに割り当てています。
    • 今回の場合はPはnumberになります。
  • K extends keyof any
    • anyで定義されているプロパティ名のみを許可するという意味になります。
    • そのためKにはプリミティブ型だけでなく、下記のような定義した型を渡すことも出来ます。
# 猫の品種の型
# 下記のHUNTER × HUNTERで書こうと思いましたが、若干理解を妨げそうだと感じたので猫にしました。
	
type CatVarietyType = "AmericanShorthair" | "Siamese" | "ScottishFold"

type CatRecordType = Record<CatVarietyType, CatType>

const CatRecord: CatRecordType = {
  AmericanShorthair: {
    id: 1,
    name: "アメリカンショートヘア",
    age: 1
  },
  Siamese: {
    id: 2,
    name: "シャム",
    age: 2
  },
  ScottishFold: {
    id: 3,
    name: "スコティッシュホールド",
    age: 3
  }
}

Pick<T,K>

  • Pick<T,K>は、Tの中からKのプロパティを持つ型を作成します。
type CharacterPickType = Pick<CharacterType, "id" | "name">

const CharacterPick: CharacterPickType = {
  id: 16,
  name: "トンパ",
}

# CharacterTypeの中からidとnameを取り出して型を作成します。
# `K`でageは指定していないのでCharacterPickTypeにageのプロパティはありません。

TypescriptのPick<T,K>の定義

type Pick<T, K extends keyof T> = {
    [P in K]: T[P];
};
  • K extends keyof T
    • Tで定義されているプロパティ名のみを許可するという意味になります。
  • P in K
    • Kの値を一つずつ取り出してPに割り当てています。
  • T[P]
    • TPというkeyの型を意味します。
    • 今回の場合Pはid, nameなので、numberとstringがそれぞれ割り当てられます。

Exclude<T,U>

  • Exclude<T,U>はTのプロパティでUに代入できないもので型を作成します。
type CharacterType = 1 | "クロロ" | 26 | "男性"

type CharacterExcludeType = Exclude<CharacterType, string>

const CharacterExclude: CharacterExcludeType = 1 || 26

# CharacterTypeの中でstringでないものだけを残して型を作成するので
CharacterExcludeTypeは126になります。

TypescriptのExclude<T,U>の定義

type Exclude<T, U> = T extends U ? never : T;
  • T extends U ? never : T
    • TU なら never, それ以外は T の型を返します。
    • CharacterTypeの1 | "クロロ" | 26 | "男性"のそれぞれがstringかどうかを検証して当てはまればnever型を返し当てはまらなければTの型を返します。
  • 今回の例でstringでないのはnumberである1と12だけなのでCharacterExcludeType = 1 | 26となります。

Extract<T,U>

  • Extract<T,U>はExclude<T,U>とは反対にTのプロパティでUに代入できるもので型を作成します。
type CharacterType = 1 | "クロロ" | 26 | "男性"

type CharacterExtractType = Extract<CharacterType, string>

const CharacterExtract: CharacterExtractType = "クロロ" || "男性"

# CharacterTypeの中でstringのものだけで型を作成するので、
CharacterExtractTypeは"クロロ""男性"になります。

TypescriptのExclude<T,U>の定義

type Extract<T, U> = T extends U ? T : never;
  • T extends U ? T : never
    • Exclude<T,U>の定義の反対でTU ならT の型, それ以外はneverを返します。
    • CharacterTypeの1 | "クロロ" | 26 | "男性"のそれぞれがstringかどうかを検証して当てはまればTの型を返し当てはまらなければnever型を返します。
  • 今回の例でstringなのは"クロロ"と"男性"だけなのでCharacterExtractType = "クロロ" || "男性"となります。

Omit<T,K>

  • Omit<T,K>は、Tの中からKのプロパティを除いた型を作成します。
  • Pick<T,K>と反対のことができます。
type CharacterOmitType = Omit<CharacterType, "id" | "name">

const CharacterOmit: CharacterOmitType = {
  age: 1,
}

# CharacterTypeの中からidとnameを除いた型を作成するので、
age: numberのみが型になります。

TypescriptのOmit<T,K>の定義

type Omit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>;
  • K extends keyof any
    • anyで定義されているプロパティ名のみを許可するという意味です。
  • Pick<T, Exclude<keyof T, K>>
    • まずExclude<keyof T, K>TであるCharacterTypeのkey(id, name, age)の中からK(id, name)に当てはまらないものを取得するのでageが残ります。
    • そうなるとPick<T, "age">になり、Pick<T,K>でTであるCharacterTypeの中からageとその型を取り出して新しい型を作成するので{age: number}が取り出されます。
    • その結果、CharacterOmitType = {age: number}となります。

おわりに

  • ここまで読んでいただきありがとうございました。
  • この記事を通して、皆様のUtilityTypesの理解の助けになればと思います。
  • 今回紹介した以外にもUtilityTypesは、まだありますのでご興味があれば公式ドキュメントをご覧ください。
  • 今回は基本的な使い方をメインで触れさせていただきましたが今後、積極的に実務でも使うことでより具体的な使い方や「こういう場面で使うと良い」など知見が溜まった時に記事を書きたいと思います。

Discussion