はじめに
本記事は 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 は こちら
その7 Mapped types は こちら
Index types
Index types を利用することで、動的にプロパティ名を扱う際にそのプロパティ名が正しいかをチェックさせることができます。
例えば、あるオブジェクトのプロパティのサブセットを作る処理を、一般的な JavaScript での書き方で書くと以下のようになります。
function pluck(o, names) {
return names.map(n => o[n]);
}
TypeScript では index type query operator と indexed access operator を利用して以下のように書きます。
function pluck<T, K extends keyof T>(o: T, names: K[]): T[K][] {
return names.map(n => o[n]);
}
interface Person {
name: string;
age: number;
}
let person: Person = {
name: 'Jarid',
age: 35
};
let strings: string[] = pluck(person, ['name']); // ok, string[]
この書き方をすることで、コンパイラは Person
という型に name
というプロパティが実際に存在するかチェックするようになります。
はじめに、 keyof T
を index type query operator と呼びます。
keyof T
は T
という型の既知の公開プロパティ名のユニオンになります。
let personProps: keyof Person; // 'name' | 'age'
keyof Person
は 'name' | 'age'
と同等です。
しかし、 Person
に address: string
というようなプロパティを追加した場合に、 keyof Person
を使っていると自動的に address が追加されて 'name' | 'age' | 'address'
になりますが、 String Literal Types を使っている場合には自動的には追加されません。
また、 keyof
は例の pluck
のような、事前にプロパティ名を知ることができない汎用的な処理でも使うことができます。
コンパイラはプロパティ名の正しいセットが pluck
に渡されているかチェックします。
pluck(person, ['age', 'unknown']); // コンパイルエラー : 'unknown' は 'name' | 'age' ではない
keyof を使っていない場合には、単なる文字列の配列なのでチェックできません。
次に、 T[K]
を indexed access operator と呼びます。
person['name']
は Person['name']
の型(ここの例だと string
)を持っていることになります。
index type queries と同様に T[K]
を汎用的に利用することができます。
以下は getProperty
という関数の例ですが、 K extends keyof T
という部分に注目してください。
function getProperty<T, K extends keyof T>(o: T, name: K): T[K] {
return o[name]; // o[name] は T[K] の型の一つ
}
getProperty
において、 o: T
で name: K
なので o[name]: T[K]
になります。
T[K]
の結果を返す際、コンパイラはキーの実際の型をインスタンス化します。そのため、 getProperty
の戻り値の型はプロパティによって異なります。
let name: string = getProperty(person, 'name');
let age: number = getProperty(person, 'age');
let unknown = getProperty(person, 'unknown'); // コンパイルエラー : 'unknown' は 'name' | 'age' に含まれない
Index types and string index signatures
keyof
と T[K]
は string index signatures に影響を受けます。
string index signature を持つ型がある場合、 keyof T
は string | number
になります。(原文では string
のみだが実際には number
も含まれる。)
interface IMap<T> { [key: string]: T; }
let keys: keyof IMap<number>; // string | number
let value: IMap<number>['foo']; // number
let value2: IMap<number>[1]; // number でのアクセス可能
そのため、 string index signature を宣言した場合には、 number index signature を宣言する必要はありません。
逆に number index signature のみを宣言した場合には、 string でのアクセスはできません。
interface IMap<T> { [index: number]: T; }
let keys: keyof IMap<number>; // number のみ
let value: IMap<number>['foo']; // エラー
let value: IMap<number>[1]; // OK