面接でいちばんよく聞かれるTypeScriptの質問を、答えられるようにしておきたい短い回答と小さな例とともに紹介します。レベル別に分けてあり、ジュニアの職種ならたいてい最初のグループまで、シニアの職種なら最後のグループの型を何も見ずに書けることが期待されます。
初級の質問
1. TypeScriptとは何ですか?
TypeScriptは、Microsoft が作った静的型付きのJavaScriptです。値に型を付けるとコンパイラーがそれをチェックし、そのあと型を取り除いて、JavaScriptが動く場所ならどこでも動く普通のJavaScriptを出力します。
@ts-expect-error のコメントがなければ、2つ目の呼び出しはコンパイルエラーになるので、このバグが世に出ることはありません。
2. TypeScriptは実行時に型をチェックしますか?
しません。型はコンパイル中に消去され、出力には跡形も残りません。上のコードが 23 と出力するのは、a が数値であるべきだと実行時には誰も知らないからです。プログラムの外から来るデータ(JSON、ユーザー入力、API のレスポンス)をチェックするには、型ガードやスキーマバリデーターのような本物のチェックを書きます。
3. 基本の型には何がありますか?
string、number(整数と小数でひとつの型。int はありません)、boolean、bigint、symbol、null、undefined、さらに配列(number[])、タプル([string, number])、オブジェクト型、any、unknown、never、void です。小文字の名前を使います。String と Number はラッパーオブジェクトの型です。
4. any と unknown の違いは何ですか?
どちらもどんな値でも受け付けます。any はチェックも無効にするので、どんな操作もコンパイルが通ります。unknown は絞り込むまで何も許さないので、まだチェックしていない値に使う安全な型です。
5. 型推論とは何ですか?
書かなかった型をコンパイラーが導き出すことです。let count = 0 は number、const mode = "dark" はリテラル型 "dark" になり、関数の戻り値の型は return 文から決まります。一般的なルールは、関数のパラメーターと公開する戻り値の型には型注釈を書き、ローカル変数は推論に任せることです。
6. interface と type の違いは何ですか?
どちらもオブジェクトの形を表し、クラスはどちらも implements できます。interface は開き直してマージでき(宣言のマージ)、ほかのインターフェースを extends できます。type エイリアスは何にでも名前を付けられます: ユニオン、タプル、プリミティブ、マップ型、条件型。
interface User { name: string }
interface User { age: number } // merged: User has name and age
type Id = string | number; // only a type alias can be a union
type Pair = [string, number]; // or a tuple
type Admin = User & { role: "admin" };
オブジェクトの形には interface、それ以外には type を使うのが一般的な慣習です。詳しい比較は interface と type の違い のページにあります。
7. ユニオン型とインターセクション型とは何ですか?
ユニオン A | B は、どちらかの型である値です。絞り込むまでは、すべての型に共通するメンバーしか使えません。インターセクション A & B は、同時に両方の型である値で、それぞれのすべてのメンバーを持ちます。
type Id = string | number; // either
type Timestamped = { createdAt: Date };
type Post = { title: string } & Timestamped; // both: title and createdAt
8. void と never の違いは何ですか?
void は、意味のある値を返さずに普通に終わる関数の戻り値の型です。never は起こりえないものの型です。常に例外を投げる関数や無限ループする関数、すべてのケースが取り除かれたユニオンがそうです。never はどの型にも代入でき、never にはどんな値も代入できません。
function log(msg: string): void { console.log(msg); }
function fail(msg: string): never { throw new Error(msg); }
type Impossible = string & number; // never
中級の質問
9. 型の絞り込みとは何ですか?
絞り込み(ナローイング)は、コンパイラーがチェックをたどって各分岐の中で型を細かくすることです。typeof、instanceof、in、等価チェック、真偽値の判定、ユーザー定義の型ガードを理解します。
10. ユーザー定義の型ガードとは何ですか?
戻り値の型が value is T の関数です。true を返すと、呼び出し側の変数が T に絞り込まれます。コンパイラーはその関数を信頼するので、本体では本当に値をチェックしなければなりません。
interface Cat { meow(): void }
function isCat(value: unknown): value is Cat {
return (
typeof value === "object" &&
value !== null &&
"meow" in value &&
typeof value.meow === "function"
);
}
アサーション関数 function assertCat(v: unknown): asserts v is Cat は、false を返す代わりに例外を投げることで絞り込みます。
11. ジェネリクスとは何ですか?
入力と出力の関係を保ったまま、ひとつの関数、クラス、型を多くの型で使えるようにする型パラメーターです。T は引数から推論され、extends でなりうる型を制約します。
"email" はユーザーの型の keyof ではないので、pluck(users, "email") はコンパイルエラーになります。
12. 型の中で keyof と typeof は何をしますか?
keyof T は T のプロパティ名のユニオンです。型の位置の typeof x は変数の型を返します。組み合わせた keyof typeof obj は、オブジェクトのキーをユニオンにします。
const colors = { red: "#f00", green: "#0f0" };
type Colors = typeof colors; // { red: string; green: string }
type ColorName = keyof typeof colors; // "red" | "green"
13. ユーティリティ型とは何ですか?いくつか挙げてください。
ほかの型を変換する組み込みのジェネリック型です。よく聞かれるもの:
| ユーティリティ型 | 結果 |
|---|---|
Partial<T> | すべてのプロパティを省略可能に |
Required<T> | すべてのプロパティを必須に |
Readonly<T> | すべてのプロパティを readonly に |
Pick<T, "a" | "b"> | 指定したプロパティだけ |
Omit<T, "a"> | 指定したもの以外のすべてのプロパティ |
Record<K, V> | キーが K、値が V のオブジェクト |
Exclude<U, X>, Extract<U, X> | ユニオンのメンバーを除く、または残す |
NonNullable<T> | null と undefined を除いた T |
ReturnType<F>, Parameters<F> | 関数の戻り値の型、パラメーターの型 |
Awaited<T> | Promise が解決する値 |
完全な一覧はユーティリティ型のページにあります。
14. 判別可能なユニオンとは何ですか?
リテラルの「タグ」プロパティを共有するオブジェクト型のユニオンです。タグをチェックすると値がひとつのメンバーに絞り込まれ、default の分岐に never のチェックを置くと、処理し忘れたケースをコンパイラーが報告します。
15. as と satisfies の違いは何ですか?
value as T は型アサーションです。値を T として扱うようコンパイラーに指示し、チェックのほとんどを省きます。value satisfies T は値を T に照らしてチェックしますが、値自身のより正確な型を保ちます。
type Theme = { primary: string; secondary: string };
const a = { primary: "#07f" } as Theme; // compiles: the missing key is not reported
const b = { primary: "#07f" } satisfies Theme; // error TS2741: Property 'secondary' is missing
const c = {} as { name: string }; // compiles; c.name is undefined at run time
オブジェクトがそろっていれば、satisfies は推論された型も保ちます。Record<string, string> に照らしてチェックしても、変数はどのキーを持っているかを正確に知っています。型注釈なら任意の文字列キーに広がってしまうところです。
16. 変数のあとの ! 演算子は何をしますか?
非 null アサーションです。el! は型から null と undefined を取り除きます。チェックは生成されないので、値が実際に null なら、使った場所でプログラムがクラッシュします。?.、??、明示的な if を使いましょう。
17. private と #private の違いは何ですか?
private はコンパイラーだけが強制し、JavaScriptの出力では普通のプロパティです。#field はJavaScriptのプライベートフィールドで、実行時に強制されます。
protected は private と同じように働きますが、サブクラスからのアクセスも許します。readonly は、生成後の再代入を禁止します。
18. 抽象クラスとインターフェースの違いは何ですか?
インターフェースは型でしかありません。形を表し、出力からは消えます。抽象クラスはインスタンス化できない本物のクラスで、サブクラスが実装しなければならない abstract メンバーと並べて、実装済みのメソッド、フィールド、コンストラクターを持てます。クラスは多くのインターフェースを実装できますが、継承できるクラスはひとつだけです。
abstract class Repository<T> {
protected items: T[] = [];
abstract validate(item: T): boolean; // subclasses must implement
add(item: T): void { // shared implementation
if (this.validate(item)) this.items.push(item);
}
}
19. TypeScriptの関数オーバーロードとは何ですか?
複数の呼び出しシグネチャと、そのすべてと互換性のあるシグネチャを持つひとつの実装です。呼び出し側からはオーバーロードしか見えないので、呼び出しごとに正確な戻り値の型が得られます。
function parse(value: string): number;
function parse(value: string[]): number[];
function parse(value: string | string[]): number | number[] {
return Array.isArray(value) ? value.map(Number) : Number(value);
}
const one = parse("4"); // number
const many = parse(["1", "2"]); // number[]
戻り値の型が引数の型に依存しないなら、ユニオンのパラメーターを持つシグネチャひとつのほうがシンプルです。
上級の質問
20. 構造的型付けとは何ですか?
TypeScriptは型を名前ではなく形で比べます。必要なプロパティを持つ値なら、その型で宣言されたことがなくても受け付けられます。
pixel は変数なので、余分な color はここでは問題になりません。オブジェクトリテラルを直接 show({ x: 3, y: 4, color: "red" }) のように渡すと余剰プロパティのエラーになります。このチェックは新しく書いたリテラルにだけ適用されます。同じ形の2つの型を混ぜてはいけない場合(どちらも文字列の UserId と OrderId)は、ブランド型を使います: type UserId = string & { readonly __brand: "UserId" }。
21. Readonly と Partial を自分で実装するには?
マップ型を使います。keyof T を反復し、各プロパティに修飾子を付けます。
type MyReadonly<T> = { readonly [K in keyof T]: T[K] };
type MyPartial<T> = { [K in keyof T]?: T[K] };
type Mutable<T> = { -readonly [K in keyof T]: T[K] }; // "-" removes a modifier
type User = { name: string; age: number };
type Draft = MyPartial<User>; // { name?: string; age?: number }
22. 条件型と infer とは何ですか?
T extends U ? X : Y は条件に基づいて型を選びます。条件の中では、infer が一致した型の一部をとらえる型変数を宣言します。条件型はユニオンに対して分配されます。
type MyReturnType<F> = F extends (...args: any[]) => infer R ? R : never;
type ElementOf<T> = T extends (infer E)[] ? E : T;
type A = MyReturnType<() => Promise<number>>; // Promise<number>
type B = ElementOf<string[]>; // string
type C = ElementOf<number | boolean[]>; // number | boolean (distributed)
ほかのパターンは条件型のページにあります。
23. .d.ts ファイルとは何で、declare は何をしますか?
型定義ファイルは、JavaScriptのライブラリやブラウザーの API のように、別の場所にあるコードの型だけを持ちます。declare は値を作らずにそれが存在することを宣言し、declare const VERSION: string; は何にもコンパイルされません。ライブラリの型はパッケージに同梱されているか @types/{name} パッケージから来ます。自分のコードの .d.ts ファイルは declaration: true で tsc が生成します。
24. strict は何をし、どの tsconfig オプションが重要ですか?
"strict": true は、noImplicitAny、strictNullChecks、strictFunctionTypes、strictBindCallApply、strictPropertyInitialization、noImplicitThis、useUnknownInCatchVariables、strictBuiltinIteratorReturn を有効にします。TypeScript 7 ではデフォルトで有効です。面接で聞かれるほかのオプション:
| オプション | 一言で答えると |
|---|---|
target | 出力がどのバージョンのJavaScriptを使うか |
module | 出力のモジュール形式: nodenext は Node のルールに従い、esnext/preserve はバンドラー向けに import を残す |
moduleResolution | import の探し方: nodenext は ES モジュールのファイルで ./file.js を求め、bundler は ./file を受け付ける |
noUncheckedIndexedAccess | arr[i] の型に undefined が含まれる |
paths | 型チェッカーだけのための import のエイリアス。出力にはエイリアスが残るので、バンドラーか Node の imports フィールドで解決する必要がある |
skipLibCheck | 速度のために .d.ts ファイルのチェックを省く |
verbatimModuleSyntax | 型としてしか使わない import には type の印が必要。ほかの import は書いたとおりに残る |
25. TypeScript 7 とは何ですか?
Go でネイティブのプログラムとして書き直されたTypeScriptのコンパイラーで、typescript npm パッケージのバージョン7としてリリースされました。コマンドは今も tsc で言語も同じですが、JavaScriptベースの 6.x のコンパイラーよりはるかに高速です。TypeScriptチームは、ネイティブコードと共有メモリのマルチスレッドにより、一般的なフルビルドで8倍から12倍の高速化を報告しています。また、6.0 で非推奨になっていたものの削除も強制します。たとえば baseUrl、outFile、moduleResolution: "node10"、module: "amd"、alwaysStrict: false は、今では削除を求めるエラー(TS5102、TS5108)になります。
よくある質問
TypeScriptの面接でいちばんよく聞かれる質問は何ですか?
よく出るのは、any と unknown の違い、interface と type の違い、ジェネリクスのしくみ、型の絞り込みと型ガードのしくみ、Partial、Pick、Omit などのユーティリティ型の働き、そしてTypeScriptが実行時に型をチェックするかどうか(しません)です。
シニアの開発者にはどんなTypeScriptの質問がされますか?
型を読むだけでなく書くことが求められます。マップ型と条件型で Readonly や ReturnType を実装する、構造的型付けとブランド型を使う場面を説明する、網羅性チェック付きの判別可能なユニオンを設計する、strict、noUncheckedIndexedAccess、モジュール解決などの tsconfig の選択について議論する、といったものです。
TypeScriptの面接にはどう準備すればよいですか?
strict を有効にして小さなプログラムを書き、コンパイラーのエラーを予測できるようになるまで読み込みます。型の絞り込み、制約付きのジェネリクス、主なユーティリティ型、型アサーションが安全でない理由を、それぞれ2行ほどの例とともに説明できるようにしておきましょう。
React や Angular の面接でTypeScriptは聞かれますか?
たいてい聞かれます。Angular 自体がTypeScriptで書かれていて、Angular のアプリもTypeScriptで書くので、Angular の面接では @Component のようなデコレーターやアクセス修飾子も含めて知っている前提になりがちです。TypeScriptのコードベースの React の面接では、props、state、イベントハンドラーに型を付けるよう求められることが多く、ここで紹介するインターフェース、ユニオン、ジェネリクスがそのまま使われます。