はじめに
本記事は TypeScript Handbook の Advanced Types に書かれているものをベースに、説明されている内容をこういう場合はどうなるのかといったことを付け加えて ちょっとだけ 掘り下げます。完全な翻訳ではなく、若干元の事例を改変しています。
今回は Discriminated Unions について掘り下げます。
その1 Type Guards and Differentiating Types は こちら
その2 Nullable types は こちら
その3 Type Aliases は こちら
その4 String Literal Types / Numeric Literal Types は こちら
その5 Discriminated Unions は こちら
その6 Index types は こちら
その7 Mapped types は こちら
Mapped types
interface Person {
name: string;
age: number;
}
上記のような既存の型を以下のように、すべてのプロパティをオプションにした型が欲しくなることがあります。
interface PersonPartial {
name?: string;
age?: number;
}
また読み取りだけ可能なものを作りたくなることがあります。
interface PersonReadonly {
readonly name: string;
readonly age: number;
}
TypeScript には元の型に基づいて新しい型を作る Mapped types という仕組みがあります。
Mapped type では元の型にあるすべてのプロパティを同じ方法を用いて変換し新しい型を作ります。
はじめに、単純な Mapped type の例を見てみましょう。
type Keys = 'option1' | 'option2';
type Flags = { [K in Keys]: boolean };
この文法は index signature を for .. in
の中で利用する文法に似ています。
-
K
という変数はループごとに各プロパティに結びつけられます。 -
Keys
という string literal types は繰り返しに利用するプロパティの名前です。 -
boolean
はプロパティの型です。
この例では Keys
はハードコーディングされたプロパティの名前のリストで、新たに作成される型のプロパティの戻り値はすべて boolean
です。これは下記の記述と同等になります。
type Flags = {
option1: boolean;
option2: boolean;
}
keyof
と index access types を利用して Readonly
や Partial
を書くと以下のようになります。
type ReadonlyPerson = { readonly [P in keyof Person]: Person[P]; };
type PartialPerson = { [P in keyof Person]?: Person[P]; };
さらに有用な一般的な書き方は以下になります。
type Readonly<T> = { readonly [P in keyof T]?: T[P]; };
type Partial<T> = { [P in keyof T]?: T[P]; };
これらの例においてプロパティ名のリストは keyof T
でプロパティの型は T[P]
です。
これは mapped type の一般的な使い方の好例です。
この種の変換は homomorhic(準同型) だからで、マッピングが T
のプロパティのみに適用され、他には適用されません。
新たに何かを追加せずとも既存のプロパティの修飾子をすべてコピーできます。
例えば Person.name
が読み取り専用であった場合に Partial<Person>.name
は読み取り専用かつオプショナルになります。
さらに Proxy<T>
で T[P]
をラップした例をみてみましょう。
type Proxy<T> = {
get(): T;
set(value: T): void;
}
type Proxify<T> = {
[P in keyof T]: Proxy<T[P]>;
}
function proxify<T>(o: T): Proxify<T> {
// ...
}
let proxyProps = proxify(props);
Readonly<T>
と Partial<T>
はとても便利なので、 TypeScript の標準ライブラリに以下の Pick
、 Record
、 Required
と共に収められています。
type Pick<T, K extends keyof T> = { [P in K]: T[P]; };
type Record<K extends string, T> = { [P in K]: T; };
type Required<T> = { [P in keyof T]-?: T[P]; };
Readonly
、 Partial
、 Pick
そしては Required
は準同型ですが、 Record
は違います。
Record
はプロパティをコピーするための型を受け取りません。
type ThreeStringProps = Record<'prop1' | 'prop2' | 'prop3', string>
これは
type ThreeStringProps = { prop1: string; prop2: string; prop3: string; };
と同義です。
準同型でない型は新しいプロパティを生成するので、どこからもプロパティの修飾子をコピーすることはできません。
Inference from mapped types
型をラップするやり方を知ったので、次はそれらをアンラップしたくなりますが、簡単にやることができます。
function unproxify<T>(t: Proxify<T>): T {
let result = {} as T;
for (const k in t) {
result[k] = t[k].get();
}
return result;
}
let originalProps = unproxify(proxyProps);
このアンラップの推論は準同型の mapped type にのみ適用可能なことに注意してください。
mapped type が準同型でない場合、アンラップ関数に明示的に型を渡す必要があります。
まとめ
標準で用意されている各 mapped type は実体に何か影響を与えることなく静的なチェックを行うことができるので非常に有用。例えば、以下のようなことができる。
interface Person {
name: string;
age: number;
}
let person = { name: 'John', age: 29 };
function readPerson(p: Readonly<Person>) {
p.age = 30; // コンパイルエラー : 代入しようとするのを止めることが可能
}
function plusAge(p: Pick<Person, 'age'>) {
p.name = 'Bob'; // コンパイルエラー : name は扱うのを止めることが可能
}