TypeScript 面接の質問
TypeScriptのインタビューでは、静的型付け、型推論、高度な型システム機能に対する理解が試されます。フロントエンド、バックエンド、フルスタックの役割で共通して出題され、スケーラブルで保守可能なコードを書けるかどうかが評価されます。概念的な説明と実践的なコーディング問題が混在します。
TypeScript 面接で問われる内容
型とインターフェース
typeエイリアスとインターフェースの違いと、それぞれを使用すべき場面を理解します。オブジェクト型、関数シグネチャ、宣言のマージをカバーします。
ジェネリック
ジェネリックパラメータを使用して、再利用可能で型安全なコンポーネントや関数を作成する方法を理解します。制約とデフォルト型を含みます。
型推論と絞り込み
TypeScriptがどのように型を推論し、条件式、判別共用体、型ガードを使用して絞り込むかを示します。
ユーティリティ型と条件付き型
組み込みのユーティリティ型(Partial、Pickなど)を活用し、カスタム条件付き型を作成して型を動的に変換します。
TypeScript 面接の質問例
- TypeScriptの `type` と `interface` の違いを説明し、どちらをどのような場合に使用するか教えてください。良い回答が押さえる点
- type はプリミティブ型やユニオン型、交差型など幅広い型の別名を作成可能
- interface はオブジェクトの形状定義に特化し、宣言のマージ(同スコープで同名のinterfaceが自動結合)をサポート
- interface はクラスの implements や extends で使用されることが多い
- type は条件型やマップ型などの高度な型操作に適している
サンプル回答を見る
type と interface はどちらもオブジェクトの型を定義できますが、最も大きな違いは、interface は同名で複数宣言すると自動的にマージされるのに対し、type はマージされず重複定義エラーになります。また、type はプリミティブ型やユニオン型、タプル型など、オブジェクト以外の型にも別名を付けられます。interface はクラスが implements できる契約として使われることが多いです。一般的には、公開APIの型や拡張性を重視する場合は interface を、複雑な型操作やユニオン型が必要な場合は type を使うと良いでしょう。具体例として、ライブラリの型定義では interface がよく使われ、条件型やマップ型を組み合わせたいときは type が必須です。ただし、どちらでも同じことができる場合はプロジェクトのコーディング規約に従うべきです。
- すべてのプロパティとネストされたプロパティをreadonlyにするジェネリック型 `DeepReadonly<T>` を実装してください。良い回答が押さえる点
- 再帰的にオブジェクトのすべてのプロパティを readonly にする
- プリミティブ型や関数はそのまま、オブジェクトは再帰処理
- 配列やタプルも考慮する必要がある
サンプル回答を見る
DeepReadonly<T> は、与えられた型 T のすべてのプロパティ(ネストされたオブジェクトを含む)を readonly にするジェネリック型です。実装には条件型と再帰的なマップ型を使用します。プリミティブ型(string, number など)や関数はそのまま返し、オブジェクトや配列の場合は各プロパティに DeepReadonly を適用します。これにより、深い階層でもイミュータブルな型を強制できます。注意点として、循環参照がある型に適用すると無限再帰になる可能性があるため、そのような型には使わないか、深さを制限する必要があります。
参考コードtypescript // プリミティブ型と関数はそのまま // オブジェクトと配列は再帰的にreadonlyにする type DeepReadonly<T> = T extends (string | number | boolean | symbol | null | undefined | Function) ? T : T extends Array<infer U> ? ReadonlyArray<DeepReadonly<U>> : T extends Map<infer K, infer V> ? ReadonlyMap<DeepReadonly<K>, DeepReadonly<V>> : T extends Set<infer S> ? ReadonlySet<DeepReadonly<S>> : { readonly [P in keyof T]: DeepReadonly<T[P]> }; // 使用例 interface User { name: string; address: { city: string; zip: number; }; } type ReadonlyUser = DeepReadonly<User>; // const user: ReadonlyUser = { name: 'Alice', address: { city: 'Tokyo', zip: 100 } }; // user.name = 'Bob'; // エラー: readonly プロパティ - 型述語に基づいてフィルタリングするジェネリック関数 `filterArray<T>(arr: T[], predicate: (item: T) => item is T): T[]` を書いてください。良い回答が押さえる点
- 型述語 (item is T) を用いてフィルタリング後に型が絞り込まれる
- 返り値の型は T[] である必要がある
- コールバックが型ガードとして機能する
- Array.prototype.filter の型定義を拡張したもの
サンプル回答を見る
この関数は、配列の各要素に対して型述語を適用し、条件を満たす要素のみを含む新しい配列を返します。型述語 (item is T) を使うことで、戻り値の配列の型が絞り込まれます。例えば、ユニオン型の配列から特定の型だけを抽出する場合に有用です。注意点として、コールバック関数の戻り値が boolean ではなく型述語である必要があり、TypeScript の型チェックが正しく動作します。この関数は Array.prototype.filter のラッパーであり、同じような型安全性を提供します。
参考コードtypescript function filterArray<T>(arr: T[], predicate: (item: T) => item is T): T[] { return arr.filter(predicate); } // 使用例 type Animal = { kind: 'dog' } | { kind: 'cat' }; const animals: Animal[] = [{ kind: 'dog' }, { kind: 'cat' }]; const dogs = filterArray(animals, (animal): animal is { kind: 'dog' } => animal.kind === 'dog'); // dogs の型は { kind: 'dog' }[] に絞り込まれる - `unknown` 型とは何ですか? `any` とどう違いますか?それぞれを使用する場面の例を挙げてください。良い回答が押さえる点
- unknown は安全なトップ型で、何でも代入できるが使用には型の絞り込みが必要
- any は型チェックを完全に無効にする
- unknown は絞り込み後のみ操作可能
- any はどんな操作も許可するが型安全性を損なう
サンプル回答を見る
unknown と any はどちらも任意の型の値を代入できますが、unknown は型安全であり、値を利用するには型ガードや型アサーションで絞り込む必要があります。一方 any は型チェックを完全にバイパスするため、誤った操作をコンパイル時に検出できません。unknown は外部から渡される値(APIレスポンスなど)を受け取る際に、後で適切に型を確定するために使います。any は既存のJavaScriptコードとの統合や、複雑な型推論が難しい場合に例外的に使われますが、可能な限り unknown を使うべきです。例:try-catch の catch 変数は unknown が推奨され、any は避けるべきです。
- ユーザーオブジェクトのPromiseを返す非同期関数に型を付けるにはどうすればよいですか?解決状態と拒否状態の処理方法を示してください。良い回答が押さえる点
- Promise の解決値の型を指定するには Promise<T> と記述
- 関数の戻り値の型として適切に型付けする
- 非同期関数は Promise を返すので、async 関数では自動的に Promise でラップされる
- エラーハンドリングは .catch() または try/catch で行う
サンプル回答を見る
ユーザーオブジェクトの Promise を返す非同期関数には、戻り値の型として Promise<User> のように型を付けます。async 関数を使う場合、戻り値の型推論は自動で行われますが、明示的に書くと良いです。解決状態では Promise が解決され、拒否状態では .catch() や try/catch でエラーを捕捉します。型安全にエラーを扱うには、カスタムエラークラスやユニオン型を使う方法もあります。例えば、関数の戻り値を Promise<User | Error> とするのではなく、拒否された Promise を catch するか、Result型を導入するのが一般的です。
参考コードtypescript interface User { id: number; name: string; } // 非同期関数の型付け function fetchUser(id: number): Promise<User> { return new Promise((resolve, reject) => { // 非同期処理 if (id > 0) { resolve({ id, name: 'Alice' }); } else { reject(new Error('Invalid ID')); } }); } // 使用例 async function handleUser() { try { const user = await fetchUser(1); console.log(user.name); } catch (error) { console.error(error); } } // .then/.catch でも同様に型安全 fetchUser(1) .then((user: User) => console.log(user.name)) .catch((error: unknown) => console.error(error)); - イベント名とペイロード型を強制する型安全なイベントエミッタークラスを設計してください。良い回答が押さえる点
- イベント名とペイロードの型をマップで管理
- on メソッドはジェネリックでイベント名に対応するハンドラ型を受け取る
- emit メソッドは指定されたイベント名のペイロードを引数に取る
- 型安全により存在しないイベントのリッスンや誤ったペイロードの送信を防止
サンプル回答を見る
型安全なイベントエミッターは、イベント名とそれに対応するペイロードの型をマッピングする型を定義します。クラスはジェネリックパラメータとしてこのマップを受け取り、on メソッドではイベント名に応じたコールバック型を強制します。emit メソッドではペイロードの型が正しいことが保証されます。これにより、存在しないイベント名を指定したり、間違ったデータ型を送信することをコンパイル時に防止できます。注意点として、型定義が複雑になりがちですが、ライブラリとして公開する際には非常に有用です。また、off や once などのメソッドも同様に型付けできます。
参考コードtypescript type EventMap = { [event: string]: unknown; }; class TypedEventEmitter<T extends EventMap> { private handlers: Map<keyof T, Array<(...args: any[]) => void>> = new Map(); on<K extends keyof T>(event: K, handler: (payload: T[K]) => void): void { const handlers = this.handlers.get(event) || []; handlers.push(handler as (...args: any[]) => void); this.handlers.set(event, handlers); } emit<K extends keyof T>(event: K, payload: T[K]): void { const handlers = this.handlers.get(event); if (handlers) { handlers.forEach(handler => handler(payload)); } } } // 使用例 interface MyEvents { login: { userId: number }; logout: void; } const emitter = new TypedEventEmitter<MyEvents>(); emitter.on('login', (data) => { // data は { userId: number } 型 console.log(data.userId); }); emitter.emit('login', { userId: 123 }); // OK // emitter.emit('logout', 'error'); // エラー: void 型の引数が必要 - `keyof` 演算子と `typeof` 演算子の動作を説明し、動的な型を作成する例を挙げてください。良い回答が押さえる点
- keyof はオブジェクト型のキーをユニオン型として取得
- typeof は値から型を抽出する
- 両者を組み合わせることで動的な型を作成可能
- keyof typeof オブジェクト でオブジェクトのキーのユニオン型を取得
サンプル回答を見る
keyof 演算子は、オブジェクト型 T のすべてのキー(プロパティ名)のユニオン型を生成します。例えば、interface User { name: string; age: number } に対して keyof User は 'name' | 'age' となります。typeof 演算子は、値(変数や定数)からその値の型を取得します。例えば、const obj = { x: 10, y: 20 } に対して typeof obj は { x: number; y: number } です。これらを組み合わせて keyof typeof obj とすると、オブジェクトのキー 'x' | 'y' を型として取得できます。これは、動的なプロパティアクセスや、オブジェクトのキーに基づく型制約を作成する際に非常に便利です。注意点として、typeof はコンパイル時ではなく実行時の値から型を推論するため、変数の型が正確でなければ意図しない型が生成されることがあります。
- マップ型を使用して、オブジェクト型 `T` のすべてのブール型プロパティを文字列リテラル 'true' | 'false' に変換する `FlagProperties<T>` 型を作成してください。良い回答が押さえる点
- マップ型 { [P in K]: T } を利用
- 条件型で boolean 型のプロパティのみを対象に変換
- 変換対象外のプロパティは元の型を維持
- 結果の型はオブジェクト型 T と同じキーを持つ
サンプル回答を見る
FlagProperties<T> は、オブジェクト型 T の中から値が boolean 型であるプロパティを抜き出し、その値の型を文字列リテラル 'true' | 'false' に変換する型です。実装にはマップ型と条件型を組み合わせます。まず T のすべてのキーに対してループし、各プロパティの値の型が boolean かどうかをチェックします。boolean の場合、そのプロパティの型を 'true' | 'false' に変更し、それ以外のプロパティは元の型を維持します。これにより、ブール値のプロパティだけが文字列リテラルユニオンに置き換えられた新しい型が得られます。注意点として、元の型が readonly や optional の場合も適切に考慮する必要があります。
参考コードtypescript type FlagProperties<T> = { [P in keyof T]: T[P] extends boolean ? 'true' | 'false' : T[P]; }; // 使用例 interface Config { debug: boolean; port: number; readonly ssl: boolean; } type ConfigWithFlags = FlagProperties<Config>; // 結果: { debug: 'true' | 'false'; port: number; readonly ssl: 'true' | 'false' } const config: ConfigWithFlags = { debug: 'true', port: 3000, ssl: 'false' };
準備方法
- ユーティリティ型(Partial、Pick、ReturnTypeなど)をゼロから実装する練習をしましょう。
- 構造的型付け(ダックタイピング)とそれが型互換性に与える影響を理解しましょう。
- 判別共用体やブランド型などの高度なパターンをマスターしましょう。
- TypeScriptプレイグラウンドや公式のチャレンジ(例:type challenges)を使ってスキルを磨きましょう。
- 実際のTypeScriptコードベース(ReactやNode.jsのライブラリなど)をレビューして、パターンが実際にどのように使われているかを見ましょう。
よくある質問
Reactの仕事にTypeScriptは必要ですか?
はい。最近のReactコードベースのほとんどがTypeScriptを使用し、エラーを早期に発見し開発者体験を向上させているからです。
インタビューに向けてTypeScriptを練習するにはどうすればよいですか?
TypeScriptプレイグラウンドを使用し、GitHubの型チャレンジ(例:type-challenges)を解き、小さなプロジェクトを実装しましょう。
TypeScriptインタビューでよくある間違いは何ですか?
`any` を多用する、strictモードを無視する、条件付き型の仕組みを理解していないことです。
TypeScriptは学ぶのが難しいですか?
基本は簡単ですが、条件付き型やテンプレートリテラル型などの高度な機能には練習が必要です。
型定義を暗記すべきですか?
暗記よりも概念を理解することに焦点を当てましょう。ジェネリックやユーティリティ型をいつ使うかを知ることが重要です。
TypeScript の質問をAIで練習、瞬時にフィードバック
履歴書をアップロードして、パーソナライズされた模擬面接を受け、改善点を確認 — 無料で始められます。