TypeScript = JavaScript + 静的型システム
TypeScriptコード
型チェック & 変換
実行可能なJS
// hello.ts
function greet(name: string): string {
return `Hello, ${name}!`;
}
// hello.js(コンパイル後)
function greet(name) {
return `Hello, ${name}!`;
}
TypeScriptの型システムはチューリング完全であり、型だけで Doomを再現可能。
function getUser(id) {
return users.find(u => u.id === id);
}
const user = getUser("123");
console.log(user.name.toUpperCase());
// ☠️ TypeError: Cannot read property 'name' of undefined
function getUser(id: string): User | undefined {
return users.find(u => u.id === id);
}
const user = getUser("123");
console.log(user.name.toUpperCase());
// ❌ Error: Object is possibly 'undefined'
導入方法、コンパイル方法、tsconfig.json
の設定、.d.ts
ファイル
# グローバル (どこでもCLI使用可能)
npm install -g typescript
# ローカル (プロジェクト推奨)
npm install --save-dev typescript
# 全てをコンパイル
>>> tsc
# 型チェックのみ (JSファイル出力なし)
>>> tsc --noEmit
# 監視モード (変更時に自動再コンパイル)
>>> tsc --watch
tsc --init
で基本的なtsconfig.jsonを自動生成できる
{
"compilerOptions": {
"target": "ES2020", // 出力されるJavaScriptのバージョン
"module": "ESNext", // 使用するモジュール方式
"outDir": "./dist", // コンパイル後のJavaScriptファイルの出力先ディレクトリ
"rootDir": "./src", // ソースのTypeScriptファイルがあるルートディレクトリ
"strict": true, // 厳格な型チェックをすべて有効にする(推奨設定)
"esModuleInterop": true // CommonJSのモジュールをESモジュールのようにimport可能にする
},
"include": ["src/**/*"], // コンパイル対象に含めるファイル
"exclude": ["node_modules"] // コンパイルから除外するファイルやフォルダ
}
// example.d.ts - 型定義ファイル
declare function myLibFunction(x: string): number;
declare const VERSION: string;
// モジュールの型定義
declare module "some-js-library" {
export function doSomething(data: any): Promise;
}
npm install @types/node
でNode.jsのAPI型定義を取得
プリミティブ型、配列、オブジェクト、any
、unknown
// 基本的なプリミティブ型
let characterName: string = "古手梨花";
let age: number = 11;
let isAlive: boolean = true;
let deathCount: null = null;
let mysteryStatus: undefined = undefined;
// symbol と bigint (ES2015+)
let fragmentId: symbol = Symbol("fragment");
let loopCount: bigint = 100000000000000000000n;
null
、undefined
、symbol
、bigint
// 配列の型定義 - 2つの書き方
let clubGrades: number[] = [1, 2, 3, 2, 1];
let clubMembers: Array<string> = ["竜宮レナ", "園崎魅音", "古手梨花", "北条沙都子"];
// 読み取り専用配列
let rules: ReadonlyArray<string> = ["鬼隠し", "綿流し", "祟殺し"];
// rules.push("暇潰し"); // エラー!
// 読み取り専用配列(readonly 修飾子)
let legends: readonly string[] = ["雛見沢症候群", "綿流しの儀式", "祟りの噂"];
// legends.pop(); // エラー!
Type[]
vs Array<Type>
- どちらでもOK、チーム規約で統一
// 混合型の配列
let evidence: (string | number)[] = ["鬼隠し", 1983, "綿流し", 1982];
// 多次元配列
let board: string[][] = [
["X", "O", "X"],
[" ", "X", "O"],
["O", " ", " "]
];
// オブジェクトの型定義
let clubMember: { name: string; age: number; weapon?: string } = {
name: "竜宮レナ",
age: 16
};
// インデックスシグネチャ
let suspicionLevels: { [character: string]: number } = {
"前原圭一": 3,
"竜宮レナ": 5,
"園崎魅音": 2
};
// ネストしたオブジェクト
let village: {
name: string;
location: {
prefecture: string;
population: number;
};
} = {
name: "雛見沢村",
location: {
prefecture: "岐阜県",
population: 2000
}
};
?マークは「省略可能」を意味し。weapon
は省略可能。
let character: {
name: string;
weapon?: string; // string | undefined
} = {
name: "古手梨花"
};
if (character.weapon) {
console.log(character.weapon.toUpperCase()); // 安全なアクセス方法:型ガード
}
console.log(character.weapon?.toUpperCase()); // 安全なアクセス方法:オプショナルチェーン
undefined
との違い
let explicitUndefined: { name: string; weapon: string | undefined } = {
name: "前原圭一",
weapon: undefined // 明示的にundefinedを設定する必要がある
};
// any型 - TypeScriptの型チェックを無効化
let mysteryData: any = "綿流し";
mysteryData = 1983;
mysteryData = true;
mysteryData = { killer: "unknown" };
// なんでもできてしまう(危険)
mysteryData.oyashiro.curse.activate; // エラーにならない
mysteryData(); // エラーにならない
mysteryData[0][1][2]; // エラーにならない
// 型の利益を失う
let result = mysteryData + 5; // resultの型もany
any
はTypeScriptの型安全性を完全に無効化する
let secret: unknown = investigate();
// 直接操作はエラー
// villageSecret.toUpperCase(); // エラー!
// villageSecret + 5; // エラー!
// 型ガードが必要
if (typeof secret === "string") {
console.log(secret.toUpperCase()); // OK
}
if (typeof secret === "number") {
console.log(secret.toFixed(2)); // OK
}
unknown
は使用前に型チェックを強制する
let mysteryA: any = "梨花";
let mysteryB: unknown = "梨花";
mysteryA.toUpperCase(); // A
mysteryB.toUpperCase(); // B
unknown
は型ガードが必要。any
は何でもOK(危険)
let clubMembers: string[] = ["圭一", "レナ", "魅音"];
let suspicionLevels: number[] = [3, 5, 2];
clubMembers.push("梨花"); // A
clubMembers.push(4); // B
suspicionLevels[0] = 10; // C
console.log(suspicionLevels.length); // D
string[]
にnumber
は追加できない
let villager: { name: string; age: number; occupation: string } = {
name: "入江京介",
age: 35,
occupation: "医師",
};
villager.name = "富竹ジロウ"; // A
villager.email = "irie@clinic.com"; // B
villager.age = "三十五歳"; // C
villager = null; // D
string
型プロパティへの代入
let gameArcs: ReadonlyArray<string> = ["鬼隠し", "綿流し", "祟殺し"];
console.log(gameArcs[0]) // A
console.log(gameArcs.length) // B
gameArcs.push("暇潰し") // C
let copy = [...gameArcs] // D
ReadonlyArray
は変更操作が禁止
// 基本的な型推論
let price = 1000; // number
let name = "商品A"; // string
let available = true; // boolean
// 配列推論
let items = [1, 2, 3]; // number[]
let mixed = [1, "text"]; // (number | string)[]
// 元のコード
export function processOrder(id: number) {
const order = { id, status: "pending", total: 0 };
return order;
}
// 生成されるd.ts
export declare function processOrder(id: number): {
id: number;
status: string;
total: number;
};
「AまたはB」の型を表現
|
(pipe) 演算子で結合
// 数値または文字列
let userId: number | string = 123;
userId = "user_456"; // 再代入OK
// 複数の型を許可
let value: string | number | boolean;
value = "hello"; // ✓
value = 42; // ✓
value = true; // ✓
value = null; // ❌ Error
function formatId(id: number | string): string {
if (typeof id === "number") {
// この中ではnumber型として扱われる
return `ID: ${id.toString().padStart(6, '0')}`;
} else {
// この中ではstring型として扱われる
return `ID: ${id.toUpperCase()}`;
}
}
formatId(123); // "ID: 000123"
formatId("abc_456"); // "ID: ABC_456"
// よく使うパターン
let message: string | null = null;
let data: User | undefined;
// null チェック後は型が絞り込まれる
if (message !== null) {
console.log(message.toUpperCase()); // string確定
}
// Optional chaining と組み合わせ
function processUser(user: User | null) {
console.log(user?.name?.toUpperCase());
}
strictNullChecks
が有効なら必須の考慮
// enum的な使い方
type Status = "pending" | "approved" | "rejected";
type HttpMethod = "GET" | "POST" | "PUT" | "DELETE";
function makeRequest(url: string, method: HttpMethod) {
// 実装
}
makeRequest("/api/users", "GET"); // ✓ Valid
makeRequest("/api/users", "PATCH"); // ❌ Error
// 数値リテラルも可能
type DiceRoll = 1 | 2 | 3 | 4 | 5 | 6;
// 共通プロパティで判別
type ApiResponse =
| { success: true; data: any }
| { success: false; error: string };
function handleResponse(response: ApiResponse) {
if (response.success) {
console.log(response.data); // dataが存在することを理解
} else {
console.log(response.error); // errorが存在することを理解
}
}
success
で型を特定
type GameResult = "win" | "lose" | "draw";
let result: GameResult;
result = "win"; // A
result = "victory"; // B
result = "draw"; // C
result = null; // D
// 基本的な関数
function add(a: number, b: number): number {
return a + b;
}
// アロー関数
const multiply = (x: number, y: number): number => x * y;
// コールバック関数の型
function processData(
data: string[],
callback: (item: string) => string
): string[] {
return data.map(callback);
}
// オプショナル引数(?を使う)
function greet(name: string, title?: string): string {
if (title) {
return `${title} ${name}`;
}
return `Hello, ${name}`;
}
// デフォルト引数
function createUser(name: string, age: number = 18) {
return { name, age }; // 戻り値の型は推論される
}
// Rest引数
function sum(...numbers: number[]): number {
return numbers.reduce((total, num) => total + num, 0);
}
function convert(input: string): number;
function convert(input: number): string;
function convert(input: string | number): string | number {
if (typeof input === "string") {
return parseInt(input, 10);
} else {
return input.toString();
}
}
void: 戻り値なし(undefined)
never: 絶対に戻らない
// void型 - undefinedを返す
function logMessage(message: string): void {
console.log(`[LOG] ${message}`); // 暗黙的にundefinedを返す
}
// voidは通常推論されるので省略可能
function saveToDatabase(data: any) {
console.log("データを保存しました"); // 戻り値の型は自動的にvoidになる
}
// void関数からundefinedは返せる
function processData(): void {
console.log("処理完了");
return; // OK
// return undefined; // これもOK
// return null; // ❌ Error
}
function throwError(message: string): never { // never型 - 例外を投げる
throw new Error(message); // この後のコードは実行されない
}
function infiniteLoop(): never { // never型 - 無限ループ
while (true) {}
}
function processValue(value: unknown): string { // 関数が途中で例外を投げる場合
if (typeof value === "string") {
return value.toUpperCase();
}
throwError("文字列以外は処理できません"); // ここでnever型を返す
}
type Color = "amethyst" | "green";
function getColorHex(color: Color): string {
switch (color) {
case "amethyst":
return "#955CD0";
case "green":
return "#8EDF5F";
default:
// 新しい色をUnionに追加したらここでエラー
const exhaustiveCheck: never = color;
throw new Error(`未処理の色: ${exhaustiveCheck}`);
}
}
undefined
を返す// A
function printHello() {
console.log("Hello!");
}
// B
function divide(a: number, b: number) {
if (b === 0) {
throw new Error("ゼロ除算エラー");
}
return a / b;
}
// C
function panic() {
throw new Error("システムエラー");
}
// D
function getUserName() {
return null;
}
今日学んだ内容を実際に使ってみよう!
関数 processId
を作成してください
id
は number
または string
のいずれかですnumber
の場合は "ID: 123"
、string
の場合は
"CODE: ABC"
の形式で返す
// テスト
console.log(processId(123)); // "ID: 123"
console.log(processId("ABC")); // "CODE: ABC"
型ガード 【type guard】を使うことができます
function processId(id: number | string): string {
// typeof演算子を使って型を判定
if (typeof id === "number") {
return `ID: ${id}`;
} else {
return `CODE: ${id}`;
}
}
関数 createMessage
を作成してください
"Hello 名前 [tag1, tag2]"
、ない場合は "Hello 名前"
を返します// テスト
console.log(createMessage("Takano")); // "Hello Takano"
console.log(createMessage("Satoko", ["admin", "user"])); // "Hello Satoko [admin, user]"
オプション引数は undefined
かもしれないので存在チェックが必要
function createMessage(name: string, tags?: string[]): string {
if (tags) { // tags?.join() でもOK
return `Hello ${name} [${tags.join(", ")}]`;
} else {
return `Hello ${name}`;
}
}
関数 investigateFragment
を作成してください
fragments
は数値の配列または文字列の配列"nanodesu"
を返す// テスト
console.log(investigateFragment([1983, 1984, 1985])); // 1983
console.log(investigateFragment(["rena", "mion"])); // "rena"
console.log(investigateFragment([])); // "nanodesu"
空配列の場合の処理がポイント
function investigateFragment(fragments: number[] | string[]): number | string | "nanodesu" {
// 空配列の場合の処理
if (fragments.length === 0) {
return "nanodesu";
}
// 型ガードで配列の型を判定
if (typeof fragments[0] === "number") {
return fragments[0]; // number配列の場合
} else {
return fragments[0]; // string配列の場合
}
}
TypeScriptの型推論を活用した書き方
// 別解1: 型推論を活用
function investigateFragment(fragments: number[] | string[]): number | string | "nanodesu" {
if (fragments.length === 0) {
return "nanodesu";
}
return fragments[0]; // TypeScriptが自動で型を推論
}
// 別解2: より簡潔な書き方
function investigateFragment(fragments: number[] | string[]): number | string | "nanodesu" {
return fragments[0] ?? "nanodesu";
}
TypeScript入門②: interface、type aliases、enum、generics、utility types