問題の背景:
いくつかのタスクでは、関数が異なるデータ構造を返す必要があります—例えば、固定長の配列(タプル)またはいくつかの結果のバリエーション(ユニオン型)。ダイナミックなJavaScriptでは、このアプローチは「結果またはエラー」というパターン(例:[err, value])によく使用され、TypeScriptでは、後続のコードが結果の構造を正しく理解するために明確な型付けが必要です。
問題:
戻り値の型を明示的に指定しない場合、TypeScriptは結果をあまりにも「広い」形式(すなわち狭くない)に持っていきます—配列またはユニオンになり、それにより開発者は自動補完や型安全性の利点を失ってしまいます。関数の結果を分解すると、要素の型に関する正確な記述を失うリスクがあります。
解決策:
条件付き型と関数のオーバーロード記述を使用し、タプルについては戻り値の配列の型を明示的に設定し、定数タプルを返す場合はas constを使用する必要があります。ユニオン結果では、ジェネリクスと型のナロイングを組み合わせて、外部のコードで戻り値の型を正しく区別できるようにします。
コードの例:
type Result<T> = [null, T] | [Error, null]; function parseNumber(str: string): Result<number> { const n = Number(str); return isNaN(n) ? [new Error('Invalid number'), null] : [null, n]; } const [err, value] = parseNumber('123'); if (err) console.error(err.message); else console.log(value!.toFixed(2)); // タプルとas constによる自動的な構造: function getStatus(flag: boolean): [string, number] | string { return flag ? ['ok', 200] as const : 'error'; } const r = getStatus(true); if (Array.isArray(r)) { // r: readonly [string, number] }
主な特徴:
分解後の配列要素の型は、常に具体的ですか?
いいえ、関数がユニオンタプルを返す場合、分解後の要素はすべての可能なバリエーションの型のユニオンを表します。
type R = [number, null] | [null, string]; const [a, b]: R = [1, null]; // a: number | null // b: null | string
as constを使用することで、追加の型記述なしにタプルの型を完全に“固定”できますか?
いいえ、as constは値を固定しますが、関数が戻り値の型なしで宣言されている場合、TypeScriptは必要以上に広い型を推論してしまう可能性があります。戻り値の型を明示的に指定する方が良いです。
function foo() { return [1, 'ok'] as const; } // foo(): readonly [1, "ok"]
異なる戻り値の型を持つオーバーロードは、分解後の結果の型を保証しますか?
オーバーロードはコンパイラーを助けますが、入力パラメータが不明な場合、結果はすべてのオプションのユニオンになります。型を正確に絞り込むには、結果を処理する際にタイプガードを使用する必要があります。
function bar(x: number): string; function bar(x: boolean): number; function bar(x: any): any { return typeof x === 'number' ? 'str' : 123; } const r = bar(Math.random() > 0.5 ? true : 1); // r: number | string
関数が配列またはエラーを返すが、それを型サインに明示的に記述していない場合。呼び出しコードでは戻り値が数値であることを期待し、result[1].toFixed(2]にアクセスすると、ランタイムエラーが発生します。
利点:
欠点:
関数がResult<T>の型で厳密に型付けされたタプルを返し、結果の処理が分解と最初の要素(エラー/ null)の明示的なランタイムチェックに基づいています。コンパイラーは、成功した型の絞り込みが行われる場合にのみ戻り値にアクセスできることを保証します。
利点:
欠点: