📝

TypeScript の Partial Type について理解する

2022/06/26に公開

TypeScript には、Utility Types という便利な型が提供されています。Utility Types の1つである Partial Type は、TypeScript 2.1 でリリースされており、Partial を使うことでプロパティに対してオプショナルな型に定義できます。つまり部分的な更新ができるため柔軟性が増し、とても便利な機能だなと思ったので、仕組みを理解するための備忘録として残しておきます。

作られた背景

Partial Type に関する Issue を見つけました。

https://github.com/microsoft/TypeScript/issues/11233

内容を見てみると、React などを含めたライブラリなどでは、オブジェクトを更新する際に特定のプロパティのみを更新したい場合があります。必要なプロパティだけを更新できると便利で、このようなユースケースに対応するため Issue が作られたようです。

以下は Issue から提案されていたコードを抜粋。

function setState<T>(target: T, settings: ??T??) {
  // ...
}
let obj = { a: 2, b: "ok", c: [1] };
setState(obj, { a: 4 }); // Desired OK
setState(obj, { a: "nope"}); // Desired error
setState(obj, { b: "OK", c: [] }); // Desired OK
setState(obj, { a: 1, d: 100 }); // Desired error
setState(obj, window); // Desired error

また、Partial Type の内部実装で Mapped Type を使っており、Mapped Type も Partial Type と同じ TypeScript 2.1 でリリースされています。Mapped Type については内部実装パートで後述します。

Partial Type の使い方

Partial Type の使い方について説明します。例えば Person という型を作ります。

type Person = {
  name: string
  age: number
  location: string
}

次に Person オブジェクトを更新できる関数を作ります。

function updatePerson(person: Person) {
  // 更新処理
}

updatePerson(person: Person)でオブジェクトを更新するには、Person オブジェクトを渡す必要があります。任意のプロパティ、例えばnameプロパティだけを更新することができません。

updatePerson({ name: 'tommykw' }) // NG。Person オブジェクトを渡す必要がある

では、Partial Type を使って Person の任意のプロパティを更新できるように修正してみます。
updatePerson(person: Person)の引数の型を、Person から Partial<Person> に変えるだけで簡単に任意のプロパティを更新できます。

function updatePerson(person: Partial<Person>) {
  // 更新処理
}

updatePerson({ name: 'tommykw' }) // OK

なお、Partial Type は再帰的なデータ構造に対応されておらず、ネストしたオブジェクトがあると次のようにコンパイルエラーとなります。再帰的なデータ構造に対応したい場合は独自に作る必要があります。

type Person = {
  name: string
  age: number
  location: { // location: string からネストしたオブジェクトに変更
    city: string
    country: string
  }
}

updatePerson({
  name: 'tommykw',
  location: {
    city: 'Tokyo', // NG。country プロパティも定義する必要がある
  },
})

トランスパイルしたコード

Partial Type をトランスパイルしてみて、どのような実装になっているのか調べてみます。

type Person = {
  name: string
  age: number
  location: string
}

const person: Partial<Person> = {
  name: 'tommykw',
}

上記に対して tsc を用いてトランスパイルした結果は次のとおりです。

"use strict";
const person = {
    name: 'tommykw',
};

トランスパイルしてみると、新しいオブジェクトが生成されていることがわかります。

Partial Type の内部実装

実際に内部の実装についてみていきます。TypeScriptes5.d.ts に Partial の定義があります。

/**
 * Make all properties in T optional
 */
type Partial<T> = {
    [P in keyof T]?: T[P];
};

それぞれ分解しながら説明します。

type Partial<T>は、ジェネリクスで任意の型を取り扱えるようにしてあります。例えば、type Person = { name: string, age: number, location: string }のプロパティをオプショナルとして使いたい場合、Partial<Person> で Partial を使えます。

keyof は型演算子で、オブジェクトのキーをユニオン型に変換できます。つまり、type Person = { name: string, age: number, location: string }keyof Personすると'name' | 'age' | 'location'に変換できます。

keyof Personで展開すると、[P in 'name' | 'age' | 'location' ]?を表します。

[P in keyof T]?: T[P];は、Mapped Type となっています。Mapped Type とは、プロパティをマッピングすることによって、既存の型から新しい型を作成できます。なお、Mapped Type はTypeScript 2.1 で導入されました

Personの場合は、name?: Person['name']age?: Person['age']location?: Person['location']とそれぞれプロパティが作成されます。

ちなみに Mapped Type には次のような形式があります。(Mapped types の PR から抜粋)

  • { [ P in K ] : T }
  • { [ P in K ] ? : T }
  • { readonly [ P in K ] : T }
  • { readonly [ P in K ] ? : T }

最終的にPartial<Person>type Person = { name?: string, age?: number, location?: string }のように全てのプロパティをオプショナルとして定義できます。

最後に

Utility Types の特に Partial Type に絞って使い方から内部実装まで仕組みに触れました。Partial Type に限らず、Utility Types は他にもたくさんあるので、学習していこうと思います。

参考資料

Discussion