TypeScript の Partial Type について理解する
TypeScript には、Utility Types という便利な型が提供されています。Utility Types の1つである Partial Type は、TypeScript 2.1 でリリースされており、Partial を使うことでプロパティに対してオプショナルな型に定義できます。つまり部分的な更新ができるため柔軟性が増し、とても便利な機能だなと思ったので、仕組みを理解するための備忘録として残しておきます。
作られた背景
Partial Type に関する Issue を見つけました。
内容を見てみると、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 の内部実装
実際に内部の実装についてみていきます。TypeScript の es5.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