インデックス型 と インデックスアクセス型
-
インデックス型(Index Signature)
→ オブジェクトが動的なキー(事前に決まっていないキー)を持つ場合に、そのキーに対する値の型を定義する方法 -
インデックスアクセス型(indexed access types)
→ オブジェクトの「キー」を使って、そのキーに対応する値の型を取得する方法
インデックス型 (Index Signature)
オブジェクトのキーが事前に決まっていない!?
オブジェクトが 「動的なキー」(事前に決まっていないキー) を持つ場合に使用される。
type ObjType = {
[K: string]: number; // 動的なキーを持ち、その値がnumber型
};
インデックス型の特徴
- フィールド名の表現部分が [K: string]
- このKの部分はキーの型変数名(任意)で、Kやkeyにするのが一般的
-
キーの型 は、
string
、number
、symbol
のみ指定できる
type ObjType = {
[K: string]: number;
};
const obj: ObjType = {
foo: 1, // foo が動的なキーになる
bar: 2 // bar が動的なキーになる
};
console.log(obj['foo']); // 1 (ブラケット記法)
console.log(obj.bar); // 2 (ドット記法)
type や interface は、複数回使いたい型を定義、型の再利用、拡張を行いたい場合に使う。
type ObjType = { // 複数回使いたい型を定義
[K: string]: number;
};
let obj1: ObjType = { a: 1 };
let obj2: ObjType = { b: 2 };
type ObjType = {
[K: string]: number;
};
type ExtendedType = ObjType & {
anotherKey: string;
};
const obj: ExtendedType = {
a: 1,
anotherKey: 'value'
};
一度きりで使う型の定義には、わざわざ type や interface を使う必要はない。
// type や interface でわざわざ定義しない
let obj: {
[K: string]: number; // 直接指定する
} = { a: 1 };
インラインで型を指定
1 度きりで使う型定義の場合、type や interface で 型定義せず、必要な型をその場で直接指定する
柔軟に型を指定する!
インデックス型は、すべてのプロパティが同じ型であることが求められる。
type ObjType= {
[K: string]: number; // すべてのプロパティがnumber型でなければならない
};
const obj: ObjType = {
a: 1, // number 型
b: 2, // number 型
c: 'hello' // エラー: string 型は許容されていない
};
[K: string]: number により、オブジェクトのプロパティが number 型に制限されている。
複数の型を許容したい場合、ユニオン型 で型を柔軟に指定する。
type ObjType = {
[K: string]: string | number; // ユニオン型
};
const obj: ObjType = {
a: 1,
b: 2,
c: 'hello'
};
undefinedに気をつけて!!
オブジェクトに動的にキーを追加できる一方、存在しないキーにアクセスしてもコンパイルエラーにはならず、undefinedが返される。
type UserType = {
[K: string]: string;
};
const user: UserType = {
name: "Taro",
country: "Japan",
};
console.log(user['name']); // Taro
console.log(user['unknownKey']); // 存在しないキーだと undefined が返される
- 存在しないキーを使用しているがコンパイルエラーにはならない
- undefined を予期せず扱ってしまうとエラーを引き起こす可能性がある
undefined対策 例1. undefinedを許容し、デフォルト値を設定しておく
type UserType = {
[K: string]: string | undefined; // undefinedを許容
};
const user: UserType = {
name: "Taro",
country: "Japan",
};
// キーが存在しない場合、`undefined` が返されるため値を設定する
const hobby = user['unknownKey'] ?? 'default';
console.log(hobby); // 'default'
?? 演算子(Null合体演算子)
を使用して、null または undefined の場合にデフォルト値を設定し安全に扱う。
undefined対策 例2. TSConfigでnoUncheckedIndexedAccessを設定する
noUncheckedIndexedAccess
インデックス型のプロパティや配列要素を参照したときundefinedのチェックを必須にするコンパイラオプション。
// TSConfig
{
"compilerOptions": {
"noUncheckedIndexedAccess": true
}
}
設定を有効にした場合、[K: string]: string と定義された型は、[K: string]: string | undefined として扱われるようになる。
type UserType = {
[K: string]: string; // [K: string]: string | undefined; と同じ扱いになる
};
const user: UserType = {
name: "Taro",
country: "Japan",
};
const hobby = user['unknownKey'] ?? 'default';
console.log(hobby); // 'default'
インデックス型 と Mapped Types
Mapped Typesの特徴
type MappedType = {
[K in keyof T]: NewType;
}
-
K:
キー -
T:
元となる型
例)オブジェクト形式 { a: number; b: string } -
NewType
: 新しく設定する型
例)boolean の場合、MappedType のすべてのプロパティの型は boolean
型のすべてのプロパティに対して型の変換や操作が可能になる。
インデックス型:
任意のキーを設定できるため、指定したキーがオブジェクトに存在しない場合、undefinedが返される可能性があり、型の安全が損なわれる場合がある。
type IndexType = {
[K: string]: number; // 任意のキーを許容
};
Mapped Types:
インデックス型とは異なり、型が決まっている場合に、型を明示的に制限し、許可されているキーのみを使用できるようにする。事前に定義されたプロパティ名に基づいて、新しい型を生成する際に使用。型の変換やプロパティ名の追加・削除などに役立つ。
// 事前に型が決まっている
type Person = {
name: string;
age: number;
};
// name と age のプロパティに対して 新しい型 boolean を生成し適用させる
type PersonFlags = {
[K in keyof Person]: boolean;
};
Mapped Types を使用する際に、keyof を使わずに、キーのユニオン型を直接使うことも可能
type SupportLang = "en" | "fr" | "it"; // キーがあらかじめ決まっている
type Butterfly = {
[K in SupportLang]: string; // "en", "fr", "it" のキーしか使えない
}
const butterflies: Butterfly = {
en: "Butterfly",
fr: "Papillon",
it: "Farfalla",
de: "Schmetterling", // エラー:'de'は'Butterfly'型には含まれない
};
Mapped Types を使用する際に、型を新たに設定する場合
type SupportLang = "en" | "fr" | "it";
type Butterfly = {
[K in SupportLang]: string; // "en", "fr", "it" のキーしか使えない
}
// Mapped Typesを使って、Butterfly 型のすべてのプロパティをstringからnumberに変換
type ButterflyNumbered = {
[K in keyof Butterfly]: number;
};
Mapped Types によって新しく生成された型 ButterflyNumbered は number 型に変換
type ButterflyNumbered = {
en: number;
fr: number;
it: number;
};
すべてのプロパティをオプショナル(任意)にしたり、readonly にしたりすることもできる。
type Butterfly = {
en: string;
fr: string;
it: string;
};
type ButterflyOptional = {
[K in keyof Butterfly]?: string; // すべてのプロパティをオプショナルに
};
type ButterflyReadonly = {
readonly [K in keyof Butterfly]: string; // すべてのプロパティをreadonlyに
};
インデックス型 | Mapped Types | |
---|---|---|
目的 | 動的なプロパティ名に型を適用 | 特定の型のプロパティに基づいて新しい型を生成 |
プロパティ名(キー) | 事前に決まっていない(動的に決まる) | 事前に定義された型のプロパティ名(keyof T)に基づいて決まる |
型安全 | キーが動的なので、undefinedチェックが必要な場合がある | 事前に定義されたプロパティ名を使うため、許可されたキーのみ使用されるので型安全 |
インデックスアクセス型( indexed access types )
アクセスして型を取ってくる!?
type A = {
foo: number
};
type Foo = A["foo"]; // number型
インデックスアクセス型
→ 型のプロパティにアクセス
→ ブラケット記法でアクセス してその型を取得
インデックスアクセス型の構文
T[K] T型のプロパティKにアクセスする
T
: オブジェクト型
K
: キー(プロパティ名)
インデックスアクセス型は、型に対してブラケット表記法を使う。
ドット記法ではないよ!
オブジェクト型["プロパティ名"];
配列型[number]; ( 配列[インデックス]; )
◯◯型 と インデックスアクセス型
オブジェクトの型 と インデックスアクセス型
type Person = {
name: string;
age: number;
};
type AgeType = Person['age']; // number型 キーを使って型を取得
let age: AgeType = 30; // OK
age = '30'; // エラー(number型ではないため)
undefinedに気をつけて!!
オブジェクトに存在しないキーを使用すると undefined が返される。
type Person = {
name: string;
age: number;
};
const person: Person = {
name: "太郎",
age: 30
};
function getPropVal(K: string) {
return person[K]; // インデックスアクセス型
}
console.log(getPropVal("name")); // "太郎"
console.log(getPropVal("age")); // 30
console.log(getPropVal("unknownKey")); // コンパイルエラーなし、undefined が返される
undefined対策 例1. undefinedを許容し、デフォルト値を設定しておく
type Person = {
name: string;
age: number;
};
const person: Person = {
name: "太郎",
age: 30
};
function getPropVal<K extends keyof Person>(key: K): string | number {
return person[K] ?? "デフォルト値"; // person[K] が undefined なら "デフォルト値"
}
console.log(getPropVal("unknownKey")); // "デフォルト値"(存在しないプロパティの場合)
?? 演算子(Null合体演算子)
を使用して、null または undefined の場合にデフォルト値を設定し安全に扱う。
ジェネリクスで使用するextendsは、型制約 を意味する。
K extends keyof Person
とする事で、K が Person 型のプロパティ名であることを制約。つまり、K は Person 型の name や age など、keyof Person のいずれかの値でなければならないという意味になる。
getPropVal関数の引数 key は Person 型のプロパティ名である必要があるため、K extends keyof Person
として型引数 K を制限する。
undefined対策 例2. TSConfigでnoUncheckedIndexedAccessを設定する
インデックス型の時と同様
undefined対策 例3. keyof typeof で型安全を確保
keyof typeof を使う目的
-
オブジェクトからプロパティ名(キー)を型として動的に取得することで、
ユニオン型にすることができる。 -
保守性向上
オブジェクトの構造が変更されても、プロパティ名(キー)を型として取得するコードが自動的に更新されるため、コードの保守性が向上。 -
型安全を高める
存在するプロパティ名(キー)のみを許可する型を作成できる。
const person = {
name: "太郎",
age: 30
};
// typeof person で person オブジェクト型 { name: string, age: number } が返される
// keyof で このオブジェクト型の プロパティ名(キー) を取得
type PersonKeys = keyof typeof person; // (リテラル型の)ユニオン型 "name" | "age"
function getPropVal(K: PersonKeys) {
return person[K]; // インデックスアクセス型として動的に型を取得
}
console.log(getPropVal("unknownKey")); // コンパイルエラー 'unkownKey' は割り当てられない
リテラル型
プリミティブ型(数値、文字列、bool値などの単純な値)の”特定の値だけを代入可能にする型”を設定できる。
let x: number;
x = 1;
↑ 数値なら 1 でも 100 でも代入できる
↓ リテラル型を用いると、1 だけが代入可能な型が作れる。厳密な型チェックになる
let x: 1;
x = 1; // OK
x = 100; // エラー
配列型とインデックスアクセス型
type StringArray = string[];
type T = StringArray[number];
const arr: StringArray = ["north", "south", "west", "east"];
const direction: T = arr[1]; // "south" を返すので T は string として型推論される
T は string 型なので、他の文字列を代入できる状態にある。
これは、インデックスアクセス型の危うさを持つ。
const direction: T = "northwest"; // 型エラーにならない
配列のインデックスアクセスで得られる型が広すぎるため、配列の具体的な要素の値を制限できない。意図しない代入を可能にしてしまう → Widening(型の拡大)
const arr = ["north", "south", "west", "east"] as const;
// arr の型は readonly ["north", "south", "west", "east"] となり、各要素はリテラル型
as const
配列の要素を リテラル型 として型推論させる事ができる。
- 配列内の要素が 固定の値 として扱われるようになる。
- 型が リテラル型 としてイミュータブル(不変)な値になる
- 意図しない変更を防ぎ、型安全を強化できる
配列型[number] に対して、typeof
を使って、配列の型を取得し、インデックスアクセス型でリテラル型を導き出す。
type DirectionFromArray = typeof arr[number];
// DirectionFromArray の型は、"north" | "south" | "west" | "east" のユニオン型
typeof 配列型[number] とすることで、arr 配列の各要素の型を リテラル型のユニオン型 ("north" | "south" | "west" | "east")として取得できる。
const direction1: DirectionFromArray = arr[0]; // "north" 型
const direction2: DirectionFromArray = arr[1]; // "south" 型
タプル型とインデックスアクセス型
配列型 と タブル型の違い
配列型 | タプル型 | |
---|---|---|
型の要素 | 各要素が同じ型 | 各要素が異なる型 |
サイズ | 可変 | 固定 |
let pets: string[] = ['dog', 'cat', 'bird']; // 各要素同じ型
pets.push('rabbit');
console.log(pets); // ['dog', 'cat', 'bird', 'rabbit'] サイズが可変
let tuple: [number, string, boolean] = [20, "Hello", true]; // 異なる型の要素を持つ
tuple.push("New item"); // エラー:要素数が変わる
tuple[0] = "Not a number"; // エラー:number型に strign型 を割り当てれない
同じ型の値ばかりをタプルにして返却する関数の場合、区別するためにタプルにラベルを付けることができる。
const coordinate: [x: number, y: number, z: number] = [10, 20, 30];
インデックス型 と インデックスアクセス型 の違い
インデックス型 | インデックスアクセス型 | |
---|---|---|
書き方 | [key: string]: type | ObjectType['key'] |
目的 | オブジェクトのキーが動的で事前に決まっていない場合に、キーに対応する値の型を定義 | オブジェクトの特定のキーにアクセスして、そのキーに対応するプロパティの型を取り出す |
参考