- TypeScript
TypeScript配列・タプルの型定義|readonlyとオブジェクト配列
TypeScriptの配列の型は「要素の型[]」(例:number[])、タプルの型は「[型, 型]」(例:[string, number])と書きます。 配列は「同じ型の要素が何個でも並ぶもの」、タプルは「位置ごとに型と個数が決まっているもの」です。書き換えを防ぎたいときは、先頭に readonly を付けます。
この記事では、number[]・Array<T>・readonly T[]・ReadonlyArray<T>・タプル・ラベル付きタプル・可変長タプル・as const の8つを1枚の比較表で整理し、オブジェクト配列の型定義と map・filter・find の戻り値の型まで一気に示します。readonly 配列に push したとき、タプルの要素数が合わないとき、noUncheckedIndexedAccess の有無で arr[0] の型がどう変わるかは、TypeScript 7.0.2 で実際に出したエラー文つきで再現します。
TypeScript配列・タプルの型の早見表
やりたいことから書き方を選べます。
| 種類 | 書き方 | 追加・変更 | 要素数 | 向いている場面 |
|---|---|---|---|---|
| 配列 | number[] |
できる | 自由 | ふつうのリスト。まずはこれ |
| 配列(ジェネリクス形式) | Array<number> |
できる | 自由 | number[] と同じ意味。書き方の好みで選ぶ |
| 読み取り専用配列 | readonly number[] |
できない | 自由 | 関数の引数、書き換えない一覧 |
| 読み取り専用配列(ジェネリクス形式) | ReadonlyArray<number> |
できない | 自由 | readonly number[] と同じ意味 |
| タプル | [string, number] |
代入はできる | 固定 | 「名前と年齢」のような決まった並び、関数から複数の値を返す |
| ラベル付きタプル | [name: string, age: number] |
代入はできる | 固定 | 各位置の意味をエディタに表示したい |
| 省略できる要素 | [string, number?] |
代入はできる | 1 または 2 | 後ろの要素がないことがある |
| 可変長タプル | [string, ...number[]] |
代入はできる | 1 以上 | 先頭だけ意味が違う並び |
| 読み取り専用タプル | readonly [string, number] |
できない | 固定 | 書き換えない組 |
| as const | ["draft", "published"] as const |
できない | 固定 | 定数の一覧。値がリテラル型のまま残る |
よく使う組み合わせも載せます。
| やりたいこと | 書き方 |
|---|---|
| 文字列か数値が入る配列 | (string | number)[](かっこが必要) |
| 二次元配列 | number[][] |
| オブジェクトの配列 | User[](type User = { id: number; name: string }) |
| 空の配列で始める | const list: string[] = []; |
| 配列の要素の型を取り出す | (typeof list)[number] |
| 定数配列から値のユニオン型を作る | (typeof statuses)[number](statuses は as const) |
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「TypeScript配列・タプルの型定義|readonlyとオブジェクト配列」
検証した環境とコマンド
| 項目 | 内容 |
|---|---|
| TypeScript | 7.0.2(npm i typescript で入った版) |
| Node.js | v22.22.2 |
| 型チェック | npx tsc --noEmit --strict --target es2022 ファイル名.ts |
| 実行 | node ファイル名.ts |
| 推論された型の確認 | npx tsc --declaration --emitDeclarationOnly で出力した .d.ts |
エラー文は tsc の出力をそのまま載せています。
配列とタプルの型をすべて書いてみる
早見表の書き方を1ファイルに並べました。そのまま貼って動きます。
// kinds.ts
export const a: number[] = [1, 2, 3]; // 配列
export const b: Array<number> = [1, 2, 3]; // 同じ意味(ジェネリクス形式)
export const c: readonly number[] = [1, 2, 3]; // 読み取り専用
export const d: ReadonlyArray<number> = [1, 2, 3]; // 同じ意味
export const e: (string | number)[] = [1, "two", 3]; // ユニオンの配列
export const f: number[][] = [[1, 2], [3]]; // 二次元配列
export const g: [string, number] = ["佐藤", 30]; // タプル
export const h: [name: string, age: number] = ["佐藤", 30]; // ラベル付きタプル
export const i: [string, number?] = ["佐藤"]; // 省略できる要素
export const j: [string, ...number[]] = ["点数", 80, 95]; // 可変長タプル(後ろが可変)
export const k: [...string[], number] = ["a", "b", 2]; // 可変長タプル(前が可変)
export const l: readonly [string, number] = ["佐藤", 30]; // 読み取り専用タプル
export const m = ["draft", "published"] as const; // as const
console.log(a, b, c, d, e, f, g, h, i, j, k, l, m);
console.log(g.length, i.length, j.length);
実行結果:
[ 1, 2, 3 ] [ 1, 2, 3 ] [ 1, 2, 3 ] [ 1, 2, 3 ] [ 1, 'two', 3 ] [ [ 1, 2 ], [ 3 ] ] [ '佐藤', 30 ] [ '佐藤', 30 ] [ '佐藤' ] [ '点数', 80, 95 ] [ 'a', 'b', 2 ] [ '佐藤', 30 ] [ 'draft', 'published' ]
2 1 3
実行時にはどれもただの JavaScript の配列です。 readonly もタプルも、型チェックのときだけ働く決まりで、実行結果には何も残りません。
number[] と Array<number> はどちらを使うか
2つは同じ意味です。公式ハンドブックでも、number[] は Array<number> の短い書き方だと説明されています(今回ページを開いて確認しました)。選び方の目安は次のとおりです。
| 書き方 | 読みやすい場面 | 気をつける点 |
|---|---|---|
number[] |
単純な型の配列 | ユニオン型はかっこで囲む:(string | number)[] |
Array<number> |
要素の型が長いとき:Array<{ id: number; name: string }> |
readonly を前に付けられない(下のエラー参照) |
かっこを忘れた場合と、readonly を Array<T> に付けた場合のエラーです。
const bad: readonly Array<number> = [1];
const wrong: string | number[] = ["a", 1];
misc.ts(6,12): error TS1354: 'readonly' type modifier is only permitted on array and tuple literal types.
misc.ts(7,7): error TS2322: Type '(string | number)[]' is not assignable to type 'string | number[]'.
Type '(string | number)[]' is not assignable to type 'number[]'.
Type 'string | number' is not assignable to type 'number'.
Type 'string' is not assignable to type 'number'.
string | number[] は「文字列、または数値の配列」という意味になります。読み取り専用にしたいときは readonly number[] か ReadonlyArray<number> のどちらかを使います。
型を書かないときに推論される型
型注釈を省いたとき、配列リテラルは「タプル」ではなく「配列」と推論されます。 .d.ts を出力して確かめました。
// infer.ts
export const nums = [1, 2, 3];
export const mixed = [1, "two", null];
export const pair = ["佐藤", 30];
export const pairConst = ["佐藤", 30] as const;
export const statuses = ["draft", "published"] as const;
export type Status = (typeof statuses)[number];
export const empty: string[] = [];
export const filled = new Array<number>(3).fill(0);
export const seq = Array.from({ length: 3 }, (_, i) => i * 2);
export function useCounter() {
let count = 0;
const inc = () => ++count;
return [count, inc];
}
export function useCounter2() {
let count = 0;
const inc = () => ++count;
return [count, inc] as const;
}
export function useCounter3(): [number, () => number] {
let count = 0;
const inc = () => ++count;
return [count, inc];
}
const [x, y] = pairConst;
export const first = x;
export const second = y;
const [head, ...tail] = [1, 2, 3];
export const h = head;
export const t = tail;
出力された型(.d.ts):
export declare const nums: number[];
export declare const mixed: (string | number | null)[];
export declare const pair: (string | number)[];
export declare const pairConst: readonly ["佐藤", 30];
export declare const statuses: readonly ["draft", "published"];
export type Status = (typeof statuses)[number];
export declare const empty: string[];
export declare const filled: number[];
export declare const seq: number[];
export declare function useCounter(): (number | (() => number))[];
export declare function useCounter2(): readonly [number, () => number];
export declare function useCounter3(): [number, () => number];
export declare const first: "佐藤";
export declare const second: 30;
export declare const h: number;
export declare const t: [number, number];
| 書いたコード | 推論された型 | 読み取れること |
|---|---|---|
[1, 2, 3] |
number[] |
要素の型の配列になる |
[1, "two", null] |
(string | number | null)[] |
要素の型がユニオンにまとまる |
["佐藤", 30] |
(string | number)[] |
タプルにはならない |
["佐藤", 30] as const |
readonly ["佐藤", 30] |
読み取り専用タプルになり、値もリテラル型になる |
return [count, inc] |
(number | (() => number))[] |
関数の戻り値もタプルにならない |
return [count, inc] as const |
readonly [number, () => number] |
タプルとして返せる |
戻り値の型を [number, () => number] と書く |
そのとおりのタプル | 書き換えできるタプルで返せる |
関数から「値と関数の組」を返すときは、as const を付けるか、戻り値の型をタプルで書きます。 何も付けないと、受け取った側で const [count, inc] = useCounter() としても、count と inc の両方が「数値または関数」になり、inc() を呼べません。
Status の行にある (typeof statuses)[number] は、配列の要素の型を取り出す書き方です。as const の配列に使うと "draft" | "published" というユニオン型になります。
readonly配列にpushするとどうなるか
readonly 配列は、追加・変更・並べ替えのメソッドが型の上で存在しない配列です。
// err_readonly.ts
const scores: readonly number[] = [70, 85, 90];
scores.push(100);
scores[0] = 0;
scores.sort();
scores.length = 0;
const editable: number[] = scores;
function total(values: number[]): number {
return values.reduce((a, b) => a + b, 0);
}
total(scores);
const statuses = ["draft", "published"] as const;
statuses.push("archived");
export {};
tsc の出力:
err_readonly.ts(2,8): error TS2339: Property 'push' does not exist on type 'readonly number[]'.
err_readonly.ts(3,1): error TS2542: Index signature in type 'readonly number[]' only permits reading.
err_readonly.ts(4,8): error TS2339: Property 'sort' does not exist on type 'readonly number[]'.
err_readonly.ts(5,8): error TS2540: Cannot assign to 'length' because it is a read-only property.
err_readonly.ts(7,7): error TS4104: The type 'readonly number[]' is 'readonly' and cannot be assigned to the mutable type 'number[]'.
err_readonly.ts(12,7): error TS4104: The type 'readonly number[]' is 'readonly' and cannot be assigned to the mutable type 'number[]'.
err_readonly.ts(15,10): error TS2339: Property 'push' does not exist on type 'readonly ["draft", "published"]'.
| コード | 意味 | 起きた操作 |
|---|---|---|
| TS2339 | そのメソッドは readonly 配列にない | push、sort(ほかに pop、shift、unshift、splice、reverse、fill も同じ) |
| TS2542 | インデックスへの代入はできない | scores[0] = 0 |
| TS2540 | length への代入はできない | scores.length = 0 |
| TS4104 | readonly 配列を、書き換えできる配列の型に渡せない | 変数への代入、関数の引数 |
いちばん出会いやすいのは TS4104 です。 readonly の配列を、引数が number[] の関数に渡すと止まります。関数の中で書き換えられるかもしれないからです。逆向き(ふつうの配列を readonly の引数に渡す)は通ります。
直し方:新しい配列を作る、引数をreadonlyにする
// fix_readonly.ts
const scores: readonly number[] = [70, 85, 90];
// 追加・変更したいときは、新しい配列を作る
const added = [...scores, 100];
const replaced = scores.map((s, i) => (i === 0 ? 0 : s));
const sorted = [...scores].sort((a, b) => b - a);
const sorted2 = scores.toSorted((a, b) => b - a); // ES2023。元の配列を変えずに並べ替える
// 受け取る側の引数を readonly にすると、普通の配列も readonly 配列も渡せる
function total(values: readonly number[]): number {
return values.reduce((a, b) => a + b, 0);
}
const plain: number[] = [1, 2, 3];
console.log(added, replaced, sorted, sorted2);
console.log(total(scores), total(plain));
console.log("元の配列:", scores);
実行結果:
[ 70, 85, 90, 100 ] [ 0, 85, 90 ] [ 90, 85, 70 ] [ 90, 85, 70 ]
245 6
元の配列: [ 70, 85, 90 ]
toSorted は ES2023 で加わったメソッドです。--target es2022 のままだと次のエラーになるので、--target es2023(または lib に es2023 を追加)で型チェックします。
fix_readonly.ts(7,24): error TS2339: Property 'toSorted' does not exist on type 'readonly number[]'.
| やりたいこと | 書き換える書き方(readonly では使えない) | 新しい配列を作る書き方(readonly でも使える) |
|---|---|---|
| 末尾に追加 | arr.push(x) |
[...arr, x] |
| 先頭に追加 | arr.unshift(x) |
[x, ...arr] |
| 1件を置き換え | arr[i] = x |
arr.map((v, idx) => (idx === i ? x : v)) / arr.with(i, x)(ES2023) |
| 1件を削除 | arr.splice(i, 1) |
arr.filter((_, idx) => idx !== i) / arr.toSpliced(i, 1)(ES2023) |
| 並べ替え | arr.sort() |
[...arr].sort() / arr.toSorted()(ES2023) |
| 逆順 | arr.reverse() |
[...arr].reverse() / arr.toReversed()(ES2023) |
(with・toSpliced・toReversed も ES2023 のメソッドです。[3, 1, 2] の readonly 配列で試すと、順に [ 9, 1, 2 ]、[ 1, 2 ]、[ 2, 1, 3 ] が返り、元の配列は [ 3, 1, 2 ] のままでした。)
関数の引数は、書き換えないなら readonly T[] にしておくのがおすすめです。 ふつうの配列も readonly の配列も受け取れて、関数の中でうっかり書き換えることも防げます。
readonlyが守るのは1階層だけ
// shallow.ts
type User = { id: number; name: string };
const users: readonly User[] = [{ id: 1, name: "佐藤" }];
// users.push(...) は書けないが、要素の中身は書き換えられる(readonly は1階層だけ)
users[0]!.name = "鈴木";
console.log(users);
実行結果(型チェックも通ります):
[ { id: 1, name: '鈴木' } ]
readonly が止めるのは「配列そのもの」への追加・差し替えです。要素のオブジェクトの中まで読み取り専用にしたいときは、readonly Readonly<User>[] と書くか、定数なら as const を使います。どちらも、要素のプロパティへの代入が次のエラーで止まります。
misc.ts(12,11): error TS2540: Cannot assign to 'name' because it is a read-only property.
タプルの要素数・順番が違うとどうなるか
タプルは、要素の個数と各位置の型を検査します。
// err_tuple.ts
const a: [string, number] = ["佐藤", 30, true];
const b: [string, number] = ["佐藤"];
const c: [string, number] = [30, "佐藤"];
const d: [string, number] = ["佐藤", 30];
d[2];
const [name, age, extra] = d;
const e: [string, number?] = ["佐藤"];
e[1].toFixed(0);
const f: [string, ...number[]] = [80, 95];
function move(x: number, y: number) {
return `${x},${y}`;
}
const point = [10, 20];
move(...point);
export {};
tsc の出力:
err_tuple.ts(1,7): error TS2322: Type '[string, number, boolean]' is not assignable to type '[string, number]'.
Source has 3 element(s) but target allows only 2.
err_tuple.ts(2,7): error TS2322: Type '[string]' is not assignable to type '[string, number]'.
Source has 1 element(s) but target requires 2.
err_tuple.ts(3,30): error TS2322: Type 'number' is not assignable to type 'string'.
err_tuple.ts(3,34): error TS2322: Type 'string' is not assignable to type 'number'.
err_tuple.ts(5,3): error TS2493: Tuple type '[string, number]' of length '2' has no element at index '2'.
err_tuple.ts(6,19): error TS2493: Tuple type '[string, number]' of length '2' has no element at index '2'.
err_tuple.ts(8,1): error TS2532: Object is possibly 'undefined'.
err_tuple.ts(9,35): error TS2322: Type 'number' is not assignable to type 'string'.
err_tuple.ts(15,6): error TS2556: A spread argument must either have a tuple type or be passed to a rest parameter.
| 行 | 起きたこと | エラー文の要点 |
|---|---|---|
| 1行目 | 要素が多い | Source has 3 element(s) but target allows only 2 |
| 2行目 | 要素が少ない | Source has 1 element(s) but target requires 2 |
| 3行目 | 順番が逆 | 位置ごとに型の不一致が出る |
| 5・6行目 | 範囲外の位置を読んだ、分割代入で3つ目を取り出した | TS2493:その位置に要素はない |
| 8行目 | 省略できる要素をそのまま使った | 型は number | undefined なので確認が必要 |
| 9行目 | 可変長タプルの先頭の型が違う | 先頭は string の約束 |
| 15行目 | ただの配列を引数に展開した | TS2556:個数がわからないので展開できない |
タプルで知っておきたい2つの動き
// tuple_pitfall.ts
// タプルでも push は型チェックを通る(要素数の検査は代入とインデックスアクセスが対象)
const pair: [string, number] = ["佐藤", 30];
pair.push(99);
console.log(pair, pair.length);
// readonly を付けると push そのものが書けなくなる
const fixed: readonly [string, number] = ["佐藤", 30];
console.log(fixed);
// スプレッドで渡すなら as const かタプル型にする
function move(x: number, y: number) {
return `${x},${y}`;
}
const point = [10, 20] as const;
const point2: [number, number] = [30, 40];
console.log(move(...point), move(...point2));
実行結果:
[ '佐藤', 30, 99 ] 3
[ '佐藤', 30 ]
10,20 30,40
1つ目:ふつうのタプルは push が通ります。 型の上では要素数2のままなのに、実行時には3つになりました。要素数を守りたいタプルには readonly を付けると、push が書けなくなって型と実際がそろいます。
2つ目:関数の引数に展開するなら、タプルにします。 [10, 20] をそのまま変数に入れると number[] と推論されて TS2556 になります。as const を付けるか、[number, number] と型を書けば展開できます。
なお、as const のタプルは読み取り専用なので、書き換えできるタプルを受け取る関数には渡せません。
function distance([x, y]: [number, number]) {
return Math.sqrt(x ** 2 + y ** 2);
}
const point = [3, 4] as const;
distance(point);
misc.ts(5,10): error TS4104: The type 'readonly [3, 4]' is 'readonly' and cannot be assigned to the mutable type '[number, number]'.
引数の型を readonly [number, number] にすれば、どちらのタプルも受け取れます。配列と同じ考え方です。
タプルの種類と length の型
| 型 | 入れられる値の例 | length の型 |
|---|---|---|
[string, number] |
["佐藤", 30] |
2 |
[name: string, age: number] |
["佐藤", 30] |
2(ラベルは表示用で、動きは同じ) |
[string, number?] |
["佐藤"]、["佐藤", 30] |
1 | 2 |
[string, ...number[]] |
["点数"]、["点数", 80, 95] |
number |
[...string[], number] |
[2]、["a", "b", 2] |
number |
省略できる要素を持つタプルの length が 1 | 2 になることは、次のエラー文で確かめました。
misc.ts(14,7): error TS2322: Type '1 | 2' is not assignable to type '3'.
タプルとオブジェクトの使い分け
| 状況 | 向いている型 | 理由 |
|---|---|---|
| 要素が2〜3個で、順番に意味がある(座標、範囲の開始と終了) | タプル | 短く書ける |
| 関数から「値と更新用の関数」を返す | タプル | 受け取る側が好きな名前を付けられる |
Object.entries や Map の「キーと値の組」 |
タプル | 標準の API がこの形 |
| 要素が4個以上、または同じ型が並ぶ | オブジェクト | 名前で取り出せて、順番の取り違えが起きない |
| 後から項目が増えそう | オブジェクト | 位置がずれない |
noUncheckedIndexedAccessでarr[0]の型はどう変わるか
arr[0] の型は、初期設定では要素の型そのまま(undefined を含まない)です。 tsconfig の noUncheckedIndexedAccess を有効にすると、| undefined が付きます。同じコードを、オプションなしとありで .d.ts に出力して比べました。
// idx.ts
const names: string[] = [];
export const first = names[0];
export const viaAt = names.at(0);
const pair: [string, number] = ["佐藤", 30];
export const p0 = pair[0];
const dict: Record<string, number> = {};
export const v = dict["x"];
| 式 | オプションなし(--strict のみ) | noUncheckedIndexedAccess あり |
|---|---|---|
names[0] |
string |
string | undefined |
names.at(0) |
string | undefined |
string | undefined |
pair[0](タプル) |
string |
string |
dict["x"](Record) |
number |
number | undefined |
読み取れることは3つです。
- strict を有効にしていても、noUncheckedIndexedAccess は有効になりません(上の「オプションなし」は --strict を付けた結果です)。別に指定します
at()は、オプションに関係なく undefined を含みます- タプルの決まった位置は、オプションを有効にしても undefined が付きません。要素があることが型でわかっているためです
空の配列から先頭を取り出して使うコードで、違いを確かめます。
// err_idx.ts
const names: string[] = [];
const first = names[0];
console.log(first.toUpperCase());
for (let i = 0; i < names.length; i++) {
console.log(names[i].toUpperCase());
}
export {};
オプションなしでは、型エラーは出ません。そのまま実行すると、3行目で TypeError: Cannot read properties of undefined (reading 'toUpperCase') になりました。noUncheckedIndexedAccess を付けると、次のように型チェックの段階でわかります。
err_idx.ts(3,13): error TS18048: 'first' is possibly 'undefined'.
err_idx.ts(5,15): error TS2532: Object is possibly 'undefined'.
5行目は i < names.length で範囲を確かめている for 文ですが、ここにもエラーが出ます。型チェックは「i が範囲内である」ことまでは追いかけないためです。書き方を少し変えると、すっきり通せます。
// fix_idx.ts
const names: string[] = ["sato", "suzuki"];
// 1. undefined を確かめてから使う
const first = names[0];
if (first !== undefined) console.log(first.toUpperCase());
// 2. オプショナルチェーンと ?? で既定値
console.log(names[5]?.toUpperCase() ?? "(なし)");
// 3. for...of や entries() ならインデックスアクセス自体が不要
for (const name of names) console.log(name.toUpperCase());
for (const [i, name] of names.entries()) console.log(i, name.toUpperCase());
// 4. 分割代入でも undefined が付く
const [head] = names;
console.log(head?.length);
実行結果(noUncheckedIndexedAccess ありで型チェックも通ります):
SATO
(なし)
SATO
SUZUKI
0 SATO
1 SUZUKI
4
| 設定 | 向いている状況 |
|---|---|
| noUncheckedIndexedAccess を有効にする | 新しく始めるコード。範囲外アクセスを型で見つけたい |
| 有効にしない | 既存のコードが多く、インデックスでのループが大量にある。まずは at() や for...of に寄せていく |
オブジェクト配列の型定義とmap・filter・findの戻り値
オブジェクトの配列は、要素の型を type で作って User[] と書きます。
// objects.ts
type User = {
id: number;
name: string;
role: "admin" | "member";
email?: string;
};
const users: User[] = [
{ id: 1, name: "佐藤", role: "admin", email: "sato@example.com" },
{ id: 2, name: "鈴木", role: "member" },
{ id: 3, name: "田中", role: "member", email: "tanaka@example.com" },
];
export const names = users.map((u) => u.name);
export const labels = users.map((u) => ({ id: u.id, label: `${u.name}さん` }));
export const members = users.filter((u) => u.role === "member");
export const found = users.find((u) => u.id === 2);
export const index = users.findIndex((u) => u.id === 2);
export const hasAdmin = users.some((u) => u.role === "admin");
export const allNamed = users.every((u) => u.name.length > 0);
export const totalId = users.reduce((sum, u) => sum + u.id, 0);
export const byId = users.reduce<Record<number, User>>((acc, u) => {
acc[u.id] = u;
return acc;
}, {});
export const emails = users.map((u) => u.email);
export const emailsOnly = users.map((u) => u.email).filter((e) => e !== undefined);
export const emailsOnly2 = users.flatMap((u) => (u.email ? [u.email] : []));
export const sorted = [...users].sort((a, b) => a.name.localeCompare(b.name, "ja"));
export const included = users.map((u) => u.id).includes(2);
console.log(names);
console.log(labels);
console.log(members.map((u) => u.name));
console.log(found?.name, index, hasAdmin, allNamed, totalId);
console.log(Object.keys(byId));
console.log(emails);
console.log(emailsOnly, emailsOnly2);
実行結果:
[ '佐藤', '鈴木', '田中' ]
[
{ id: 1, label: '佐藤さん' },
{ id: 2, label: '鈴木さん' },
{ id: 3, label: '田中さん' }
]
[ '鈴木', '田中' ]
鈴木 1 true true 6
[ '1', '2', '3' ]
[ 'sato@example.com', undefined, 'tanaka@example.com' ]
[ 'sato@example.com', 'tanaka@example.com' ] [ 'sato@example.com', 'tanaka@example.com' ]
.d.ts に出力された戻り値の型を表にまとめます。
| メソッド | 例 | 戻り値の型 | ポイント |
|---|---|---|---|
| map | users.map((u) => u.name) |
string[] |
コールバックが返す型の配列 |
| map(オブジェクトを返す) | users.map((u) => ({ id: u.id, label: ... })) |
{ id: number; label: string }[] |
新しい形の型も推論される |
| filter | users.filter((u) => u.role === "member") |
User[] |
要素の型は変わらない |
| find | users.find((u) => u.id === 2) |
User | undefined |
見つからない場合があるので undefined が付く |
| findIndex | users.findIndex(...) |
number |
見つからないと -1 |
| some / every | users.some(...) |
boolean |
|
| reduce(合計) | users.reduce((sum, u) => sum + u.id, 0) |
number |
初期値の型で決まる |
| reduce(オブジェクトに集める) | users.reduce<Record<number, User>>(..., {}) |
Record<number, User> |
型引数で結果の型を指定する |
| map(省略できるプロパティ) | users.map((u) => u.email) |
(string | undefined)[] |
undefined が混ざる |
| filter で undefined を除く | .filter((e) => e !== undefined) |
string[] |
条件から型が絞り込まれる |
| flatMap | users.flatMap((u) => (u.email ? [u.email] : [])) |
string[] |
map と絞り込みを一度に書ける |
| sort | [...users].sort(...) |
User[] |
元の配列を変えないようコピーしてから並べ替える |
| includes | ids.includes(2) |
boolean |
filter((e) => e !== undefined) の結果が string[] に絞り込まれるのは、条件式から型の絞り込みを推論する仕組みによるものです(TypeScript 5.5 で加わった動きとして知られています。今回は 7.0.2 で確認しました)。filter(Boolean) と書いた場合は (string | undefined)[] のままで、string[] に代入すると TS2322 になりました。そのため、undefined を除きたいときは !== undefined と比較を書くか、flatMap を使います。
オブジェクト配列でよく出るエラー
// err_objects.ts
type User = { id: number; name: string; role: "admin" | "member" };
const users: User[] = [
{ id: 1, name: "佐藤", role: "admin" },
{ id: 2, name: "鈴木", role: "guest" },
{ id: 3, name: "田中" },
{ id: 4, name: "高橋", role: "member", age: 30 },
];
const found = users.find((u) => u.id === 9);
console.log(found.name);
const list = [];
list.push(1);
const statuses = ["draft", "published"] as const;
function isStatus(value: string) {
return statuses.includes(value);
}
export {};
err_objects.ts(4,24): error TS2322: Type '"guest"' is not assignable to type '"admin" | "member"'.
err_objects.ts(5,3): error TS2741: Property 'role' is missing in type '{ id: number; name: string; }' but required in type 'User'.
err_objects.ts(6,40): error TS2353: Object literal may only specify known properties, and 'age' does not exist in type 'User'.
err_objects.ts(9,13): error TS18048: 'found' is possibly 'undefined'.
err_objects.ts(15,28): error TS2345: Argument of type 'string' is not assignable to parameter of type '"draft" | "published"'.
| コード | 意味 | 直し方 |
|---|---|---|
| TS2322 | 値が決められた候補にない | 候補の値にするか、型に候補を足す |
| TS2741 | 必須のプロパティがない | プロパティを足すか、型で role?: にする |
| TS2353 | 型にないプロパティを書いた | 不要なら消す。必要なら型に足す |
| TS18048 | find の結果は undefined かもしれない | found?.name、または if (found) で確かめる |
| TS2345 | as const の配列の includes に、ただの string を渡した | 下の書き方で解消する |
エラーの位置が、配列全体ではなく「合わない要素のその場所」に出る点が、User[] と型を書いておく良さです。なお、const list = [] は今回エラーになりませんでした。push した値に合わせて型が広がっていくためですが、何が入る配列かが読み取りにくいので、空の配列で始めるときは const list: number[] = [] と型を書くのがおすすめです。
as constの配列でincludesを使う
// includes_fix.ts
const statuses = ["draft", "published"] as const;
type Status = (typeof statuses)[number];
// 配列側を readonly string[] として見ることで、任意の文字列を調べられる
function isStatus(value: string): value is Status {
return (statuses as readonly string[]).includes(value);
}
const input: string = "published";
if (isStatus(input)) {
const s: Status = input;
console.log("OK:", s);
}
console.log(isStatus("archived"));
実行結果:
OK: published
false
戻り値を value is Status と書いておくと、if の中で input が "draft" | "published" として扱えます。外から来た文字列を、決まった値のどれかだと確かめる定番の書き方です。
配列・タプルの型定義チェックリスト
| 確認すること | 目安 |
|---|---|
| ユニオンの配列をかっこで囲んだか | (string | number)[] |
| 空の配列に型を書いたか | const list: string[] = [] |
| 書き換えない引数を readonly にしたか | values: readonly number[] |
| 関数からタプルを返すとき as const か戻り値の型を書いたか | 省くと配列に広がる |
| 要素数を守りたいタプルに readonly を付けたか | ふつうのタプルは push が通る |
| find の結果を undefined の確認なしで使っていないか | found?.name |
arr[0] の undefined を型で見つけたいか |
noUncheckedIndexedAccess を検討 |
| 定数の一覧に as const を付けたか | (typeof list)[number] でユニオン型が作れる |
よくある質問
Q. number[] と Array<number> に違いはありますか? A. 意味は同じです。短く書ける number[] が使われることが多く、要素の型が長いときは Array<...> のほうが読みやすくなります。チームで書き方をそろえておくと迷いません。
Q. readonly と as const はどう違いますか? A. readonly は型に付ける指定で、「この配列は書き換えない」という決まりだけを加えます。as const は値に付ける指定で、読み取り専用のタプルにしたうえで、中の値もリテラル型("draft" など)のまま残します。定数の一覧からユニオン型を作りたいときは as const を使います。
Q. タプルに push できてしまうのはなぜですか? A. タプルの型は配列のメソッドをそのまま持っていて、要素数の検査は代入とインデックスでの読み書きが対象だからです。readonly を付けると push が使えなくなり、要素数が保たれます。
Q. noUncheckedIndexedAccess は有効にしたほうがよいですか?
A. 新しく書き始めるコードなら有効にする価値があります。arr[0] が undefined かもしれないことを型が教えてくれます。既存のコードが多い場合は、for...of や at() に書き換えながら少しずつ進めると無理がありません。
まとめ
- 配列は
number[](=Array<number>)、タプルは[string, number]。ユニオンの配列は(string | number)[]とかっこで囲む - 型を書かない配列リテラルはタプルにならない。タプルにしたいときは型を書くか as const を付ける
- readonly 配列への push は TS2339、書き換えできる型への受け渡しは TS4104。追加・並べ替えは新しい配列を作り、引数は
readonly T[]で受け取る - タプルの要素数違反は「Source has 3 element(s) but target allows only 2」。ふつうのタプルは push が通るので、要素数を守るなら readonly を付ける
arr[0]は初期設定では undefined を含まない。noUncheckedIndexedAccess を有効にするとT | undefinedになり、タプルの決まった位置はそのまま- オブジェクト配列は
User[]。map はコールバックの戻り値の配列、filter はUser[]、find はUser | undefinedを返す
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「TypeScript配列・タプルの型定義|readonlyとオブジェクト配列」
株式会社RJCでは、TypeScriptの配列とタプルの型定義を仲間と学び合っています。こうした知識を共有しながら、日々技術を磨いています。
株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。
WE ARE HIRING
RJCで一緒に開発しながら、
成長を楽しみませんか?
RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!
- 127日年間休日
- 12時間平均残業時間
- 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!
ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。