Posted at

TypeScript Handbook の Advanced Types をちょっとだけ掘り下げる - その6 Index types

More than 1 year has passed since last update.


はじめに

本記事は TypeScript HandbookAdvanced 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 operatorindexed 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 Tindex type query operator と呼びます。

keyof TT という型の既知の公開プロパティ名のユニオンになります。

let personProps: keyof Person; // 'name' | 'age'

keyof Person'name' | 'age' と同等です。

しかし、 Personaddress: 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: Tname: 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

keyofT[K] は string index signatures に影響を受けます。

string index signature を持つ型がある場合、 keyof Tstring | 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