TypeScriptへの取り組み
本書をサポートしてください:購入 または 寄付
(広告、ブロックしないでください。)

19 配列の型付け



この章では、TypeScriptで配列をどのように型付けできるかを調べます。

19.1 配列の役割

配列はJavaScriptにおいて以下の役割を果たすことができます(1つまたは複数の役割を組み合わせることもできます)。

TypeScriptは、これらの2つの役割に対応するために、様々な配列の型付け方法を提供しています。次にそれらを見ていきましょう。

19.2 配列の型付け方法

19.2.1 配列の役割「リスト」: 配列型リテラル vs. インターフェース型 `Array`

配列型リテラルは、要素型に `[]` を付けたものです。以下のコードでは、配列型リテラルは `string[]` です。

// Each Array element has the type `string`:
const myStringArray: string[] = ['fee', 'fi', 'fo', 'fum'];

配列型リテラルは、グローバルなジェネリックインターフェース型 `Array` を使用する簡略記法です。

const myStringArray: Array<string> = ['fee', 'fi', 'fo', 'fum'];

要素型がより複雑な場合は、配列型リテラルに括弧が必要です。

(number|string)[]
(() => boolean)[]

この場合、ジェネリック型 `Array` の方が適しています。

Array<number|string>
Array<() => boolean>

19.2.2 配列の役割「タプル」: タプル型リテラル

配列の長さが固定されていて、各要素がその位置によって異なる固定された型を持つ場合は、`[string, string, boolean]` などのタプル型リテラルを使用できます。

const yes: [string, string, boolean] = ['oui', 'sí', true];

19.2.3 配列のようなオブジェクト: インデックスシグネチャを持つインターフェース

インターフェースにインデックスシグネチャのみがある場合、配列に使用できます。

interface StringArray {
  [index: number]: string;
}
const strArr: StringArray = ['Huey', 'Dewey', 'Louie'];

インデックスシグネチャとプロパティシグネチャの両方を持つインターフェースは、オブジェクトに対してのみ機能します(インデックス付き要素とプロパティを同時に定義する必要があるため)。

interface FirstNamesAndLastName {
  [index: number]: string;
  lastName: string;
}

const ducks: FirstNamesAndLastName = {
  0: 'Huey',
  1: 'Dewey',
  2: 'Louie',
  lastName: 'Duck',
};

19.3 落とし穴: 型推論は常に配列の型を正しく取得するとは限らない

19.3.1 配列の型の推論は難しい

配列の2つの役割のために、TypeScriptが常に正しい型を推測することは不可能です。例として、変数 `fields` に代入される以下の配列リテラルを考えてみましょう。

const fields: Fields = [
  ['first', 'string', true],
  ['last', 'string', true],
  ['age', 'number', false],
];

`fields` の最適な型は何でしょうか?以下のすべてが妥当な選択肢です。

type Fields = Array<[string, string, boolean]>;
type Fields = Array<[string, ('string'|'number'), boolean]>;
type Fields = Array<Array<string|boolean>>;
type Fields = [
  [string, string, boolean],
  [string, string, boolean],
  [string, string, boolean],
];
type Fields = [
  [string, 'string', boolean],
  [string, 'string', boolean],
  [string, 'number', boolean],
];
type Fields = [
  Array<string|boolean>,
  Array<string|boolean>,
  Array<string|boolean>,
];

19.3.2 空でない配列リテラルの型推論

空でない配列リテラルを使用する場合、TypeScriptのデフォルトはリスト型(タプル型ではない)を推論することです。

// %inferred-type: (string | number)[]
const arr = [123, 'abc'];

しかし、それが常に望ましいとは限りません。

function func(p: [number, number]) {
  return p;
}
// %inferred-type: number[]
const pair1 = [1, 2];

// @ts-expect-error: Argument of type 'number[]' is not assignable to
// parameter of type '[number, number]'. [...]
func(pair1);

型注釈を `const` 宣言に追加することで、型推論を回避できます。

const pair2: [number, number] = [1, 2];
func(pair2); // OK

19.3.3 空の配列リテラルの型推論

空の配列リテラルで変数を初期化する場合、TypeScriptは最初は `any[]` という型を推論し、変更を加えるにつれてその型を段階的に更新します。

// %inferred-type: any[]
const arr1 = [];

arr1.push(123);
// %inferred-type: number[]
arr1;

arr1.push('abc');
// %inferred-type: (string | number)[]
arr1;

初期に推論された型は、後に行われる処理の影響を受けないことに注意してください。

`.push()` の代わりに代入を使用しても、同じように動作します。

// %inferred-type: any[]
const arr1 = [];

arr1[0] = 123;
// %inferred-type: number[]
arr1;

arr1[1] = 'abc';
// %inferred-type: (string | number)[]
arr1;

対照的に、配列リテラルに少なくとも1つの要素がある場合、要素型は固定され、後で変更されません。

// %inferred-type: number[]
const arr = [123];

// @ts-expect-error: Argument of type '"abc"' is not assignable to
// parameter of type 'number'. (2345)
arr.push('abc');

19.3.4 `const` アサーションと配列の型推論

配列リテラルに`const` アサーション を追加できます。

// %inferred-type: readonly ["igneous", "metamorphic", "sedimentary"]
const rockCategories =
  ['igneous', 'metamorphic', 'sedimentary'] as const;

`rockCategories` は変更されないことを宣言しています。これには以下の影響があります。

`const` アサーションの有無による配列リテラルのさらなる例を示します。

// %inferred-type: readonly [1, 2, 3, 4]
const numbers1 = [1, 2, 3, 4] as const;
// %inferred-type: number[]
const numbers2 = [1, 2, 3, 4];

// %inferred-type: readonly [true, "abc"]
const booleanAndString1 = [true, 'abc'] as const;
// %inferred-type: (string | boolean)[]
const booleanAndString2 = [true, 'abc'];
19.3.4.1 `const` アサーションの潜在的な落とし穴

`const` アサーションには2つの潜在的な落とし穴があります。

まず、推論された型は可能な限り狭くなります。これにより、`let` で宣言された変数に問題が発生します。初期化に使用したもの以外のタプルを代入することはできません。

let arr = [1, 2] as const;

arr = [1, 2]; // OK

// @ts-expect-error: Type '3' is not assignable to type '2'. (2322)
arr = [1, 3];

第二に、`as const` を介して宣言されたタプルは変更できません。

let arr = [1, 2] as const;

// @ts-expect-error: Cannot assign to '1' because it is a read-only
// property. (2540)
arr[1] = 3;

これは良い点でも悪い点でもありませんが、それが起こることを認識しておく必要があります。

19.4 落とし穴: TypeScriptはインデックスが範囲外になることはないと仮定する

インデックスを介して配列要素にアクセスするたびに、TypeScriptは常にインデックスが範囲内にあると仮定します(A行)。

const messages: string[] = ['Hello'];

// %inferred-type: string
const message = messages[3]; // (A)

この仮定により、`message` の型は `string` になります。そして、予想されるかもしれない `undefined` または `undefined|string` ではありません。

タプル型を使用するとエラーが発生します。

const messages: [string] = ['Hello'];

// @ts-expect-error: Tuple type '[string]' of length '1' has no element
// at index '1'. (2493)
const message = messages[1];

`as const` はタプル型が推論されるため、同じ効果があります。