0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

TypeScriptのインデックス型とインデックスアクセス型:名前が似ているけど?

Posted at

インデックス型 と インデックスアクセス型

  • インデックス型(Index Signature)
    → オブジェクトが動的なキー(事前に決まっていないキー)を持つ場合に、そのキーに対する値の型を定義する方法
  • インデックスアクセス型(indexed access types)
    → オブジェクトの「キー」を使って、そのキーに対応する値の型を取得する方法

インデックス型 (Index Signature)

:point_right: オブジェクトのキーが事前に決まっていない!?

オブジェクトが 「動的なキー」(事前に決まっていないキー) を持つ場合に使用される。

例)インデックス型
type ObjType = {
  [K: string]: number; // 動的なキーを持ち、その値がnumber型
};

インデックス型の特徴

  • フィールド名の表現部分が [K: string]
  • このKの部分はキーの型変数名(任意)で、Kやkeyにするのが一般的
  • キーの型 は、stringnumbersymbolのみ指定できる
例)インデックス型
type ObjType = {
  [K: string]: number;
};

const obj: ObjType = {
  foo: 1, // foo が動的なキーになる
  bar: 2  // bar が動的なキーになる
};

console.log(obj['foo']); // 1 (ブラケット記法)
console.log(obj.bar); // 2 (ドット記法)

typeinterface は、複数回使いたい型を定義、型の再利用、拡張を行いたい場合に使う。

例)typeを使って型を再利用
type ObjType = { // 複数回使いたい型を定義
  [K: string]: number; 
};

let obj1: ObjType = { a: 1 };
let obj2: ObjType = { b: 2 };
例)typeで交差型(&)を使って型を拡張
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 で 型定義せず、必要な型をその場で直接指定する

:point_right: 柔軟に型を指定する!

インデックス型は、すべてのプロパティが同じ型であることが求められる。

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'
};

:point_right: 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対策 :arrow_right: 例1. undefinedを許容し、デフォルト値を設定しておく

例)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対策 :arrow_right: 例2. TSConfigでnoUncheckedIndexedAccessを設定する

noUncheckedIndexedAccess
インデックス型のプロパティや配列要素を参照したときundefinedのチェックを必須にするコンパイラオプション。

// TSConfig
{
  "compilerOptions": {
    "noUncheckedIndexedAccess": true
  }
}

設定を有効にした場合、[K: string]: string と定義された型は、[K: string]: string | undefined として扱われるようになる。

例)noUncheckedIndexedAccessを設定した場合
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'

:point_right: インデックス型 と Mapped Types

Mapped Typesの特徴
type MappedType = {
 [K in keyof T]: NewType;
} 

  • K: キー
  • T: 元となる型
    例)オブジェクト形式 { a: number; b: string }
  • NewType: 新しく設定する型
    例)boolean の場合、MappedType のすべてのプロパティの型は boolean

型のすべてのプロパティに対して型の変換や操作が可能になる。

:ballot_box_with_check: インデックス型
任意のキーを設定できるため、指定したキーがオブジェクトに存在しない場合、undefinedが返される可能性があり、型の安全が損なわれる場合がある。

例)インデックス型
type IndexType = {
  [K: string]: number;  // 任意のキーを許容
};

:ballot_box_with_check: Mapped Types
インデックス型とは異なり、型が決まっている場合に、型を明示的に制限し、許可されているキーのみを使用できるようにする。事前に定義されたプロパティ名に基づいて、新しい型を生成する際に使用。型の変換やプロパティ名の追加・削除などに役立つ。

例)Mapped Types
// 事前に型が決まっている
type Person = {
  name: string;
  age: number;
};
// name と age のプロパティに対して 新しい型 boolean を生成し適用させる 
type PersonFlags = {
  [K in keyof Person]: boolean; 
};

Mapped Types を使用する際に、keyof を使わずに、キーのユニオン型を直接使うことも可能

例)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 を使用する際に、型を新たに設定する場合

例)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 にしたりすることもできる。

例)Mapped Types で オプショナル や 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 )

:point_right: アクセスして型を取ってくる!?

例)インデックスアクセス型
type A = { 
    foo: number 
};

type Foo = A["foo"]; // number型

インデックスアクセス型
型のプロパティにアクセス
ブラケット記法でアクセス してその型を取得

インデックスアクセス型の構文
T[K] T型のプロパティKにアクセスする
T: オブジェクト型
K: キー(プロパティ名)

インデックスアクセス型は、型に対してブラケット表記法を使う。
:poop: ドット記法ではないよ!

オブジェクト型["プロパティ名"];
配列型[number]; ( 配列[インデックス]; )

◯◯型 と インデックスアクセス型

:one: オブジェクトの型 と インデックスアクセス型

例)オブジェクトの型 と インデックスアクセス型
type Person = {
  name: string;
  age: number;
};

type AgeType = Person['age'];  // number型 キーを使って型を取得
let age: AgeType = 30;  // OK
age = '30';  // エラー(number型ではないため)

:point_right: undefinedに気をつけて!!

オブジェクトに存在しないキーを使用すると 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対策 :arrow_right: 例1. undefinedを許容し、デフォルト値を設定しておく

例)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対策 :arrow_right: 例2. TSConfigでnoUncheckedIndexedAccessを設定する

インデックス型の時と同様

undefined対策 :arrow_right: 例3. keyof typeof で型安全を確保

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; // エラー

:two: 配列型とインデックスアクセス型

例)配列型とインデックスアクセス型
type StringArray = string[];
type T = StringArray[number];
const arr: StringArray = ["north", "south", "west", "east"];
const direction: T = arr[1]; // "south" を返すので T は string として型推論される

:warning: T は string 型なので、他の文字列を代入できる状態にある。 :warning:
これは、インデックスアクセス型の危うさを持つ

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" 型

:three: タプル型とインデックスアクセス型

配列型 と タブル型の違い

  配列型 タプル型
型の要素 各要素が同じ型 各要素が異なる型
サイズ 可変 固定
例)配列型
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']
目的 オブジェクトのキーが動的で事前に決まっていない場合に、キーに対応する値の型を定義 オブジェクトの特定のキーにアクセスして、そのキーに対応するプロパティの型を取り出す

参考

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?