diff --git a/app/pagesList.ts b/app/pagesList.ts index 0a0c675..42db5ab 100644 --- a/app/pagesList.ts +++ b/app/pagesList.ts @@ -56,6 +56,21 @@ export const pagesList = [ { id: 10, title: "非同期処理②: Async/Await" }, ], }, + { + id: "typescript", + lang: "TypeScript", + description: "にゃー", + pages: [ + { id: 1, title: "TypeScriptへようこそ" }, + { id: 2, title: "基本的な型と型推論" }, + { id: 3, title: "オブジェクト、インターフェース、型エイリアス" }, + { id: 4, title: "関数の型定義" }, + { id: 5, title: "型を組み合わせる" }, + { id: 6, title: "ジェネリクス" }, + { id: 7, title: "クラスとアクセス修飾子" }, + { id: 8, title: "非同期処理とユーティリティ型" }, + ], + }, { id: "cpp", lang: "C++", diff --git a/app/terminal/editor.tsx b/app/terminal/editor.tsx index f80b0a8..4e329a9 100644 --- a/app/terminal/editor.tsx +++ b/app/terminal/editor.tsx @@ -89,7 +89,7 @@ export function EditorComponent(props: EditorProps) { const { files, writeFile } = useEmbedContext(); const code = files[props.filename] || props.initContent; useEffect(() => { - if (!files[props.filename]) { + if (!files[props.filename] && props.initContent) { writeFile({ [props.filename]: props.initContent }); } }, [files, props.filename, props.initContent, writeFile]); diff --git a/app/terminal/page.tsx b/app/terminal/page.tsx index a4e7389..d209e91 100644 --- a/app/terminal/page.tsx +++ b/app/terminal/page.tsx @@ -53,6 +53,7 @@ interface SampleConfig { replInitContent?: string; // ReplOutput[] ではない。stringのパースはruntimeが行う editor: Record | false; exec: string[] | false; + readonlyFiles?: string[]; } const sampleConfig: Record = { python: { @@ -65,8 +66,7 @@ const sampleConfig: Record = { }, ruby: { repl: true, - replInitContent: - 'irb(main):001:0> puts "Hello, World!"\nHello, World!', + replInitContent: 'irb(main):001:0> puts "Hello, World!"\nHello, World!', editor: { "main.rb": 'puts "Hello, World!"', }, @@ -83,10 +83,12 @@ const sampleConfig: Record = { typescript: { repl: false, editor: { - "main.ts": + // main.tsにすると出力ファイルがjavascriptのサンプルと被る + "main2.ts": 'function greet(name: string): void {\n console.log("Hello, " + name + "!");\n}\n\ngreet("World");', }, - exec: ["main.ts"], + exec: ["main2.ts"], + readonlyFiles: ["main2.js"], }, cpp: { repl: false, @@ -131,6 +133,15 @@ function RuntimeSample({ {config.exec && ( )} + {config.readonlyFiles?.map((filename) => ( + + ))} ); } diff --git a/app/terminal/typescript/runtime.tsx b/app/terminal/typescript/runtime.tsx index 5f87f5c..57dcc8c 100644 --- a/app/terminal/typescript/runtime.tsx +++ b/app/terminal/typescript/runtime.tsx @@ -1,6 +1,6 @@ "use client"; -import type { CompilerOptions } from "typescript"; +import type { ScriptTarget, CompilerOptions } from "typescript"; import type { VirtualTypeScriptEnvironment } from "@typescript/vfs"; import { createContext, @@ -14,7 +14,11 @@ import { useEmbedContext } from "../embedContext"; import { ReplOutput } from "../repl"; import { RuntimeContext } from "../runtime"; -export const compilerOptions: CompilerOptions = {}; +export const compilerOptions: CompilerOptions = { + lib: ["ESNext", "WebWorker"], + target: 10 satisfies ScriptTarget.ES2023, + strict: true, +}; const TypeScriptContext = createContext<{ init: () => void; @@ -78,7 +82,12 @@ export function TypeScriptProvider({ children }: { children: ReactNode }) { } export function useTypeScript(jsEval: RuntimeContext): RuntimeContext { - const { init, tsEnv } = useContext(TypeScriptContext); + const { init: tsInit, tsEnv } = useContext(TypeScriptContext); + const { init: jsInit } = jsEval; + const init = useCallback(() => { + tsInit(); + jsInit?.(); + }, [tsInit, jsInit]); const { writeFile } = useEmbedContext(); const runFiles = useCallback( @@ -129,6 +138,10 @@ export function useTypeScript(jsEval: RuntimeContext): RuntimeContext { ) ); + for (const filename of Object.keys(files)) { + tsEnv.deleteFile(filename); + } + console.log(emitOutput); const jsOutputs = jsEval.runFiles( [emitOutput.outputFiles[0].name], @@ -149,5 +162,5 @@ export function useTypeScript(jsEval: RuntimeContext): RuntimeContext { } function getCommandlineStr(filenames: string[]) { - return `tsc ${filenames.join(" ")}`; + return `npx tsc ${filenames.join(" ")} && node ${filenames[0].replace(/\.ts$/, ".js")}`; } diff --git a/public/docs/typescript-1.md b/public/docs/typescript-1.md new file mode 100644 index 0000000..bd9b73c --- /dev/null +++ b/public/docs/typescript-1.md @@ -0,0 +1,136 @@ +# 第1章: TypeScriptへようこそ + +JavaScriptの経験がある皆さん、TypeScriptの世界へようこそ。 +この章では、TypeScriptがどのような言語であるか、なぜ現代のWeb開発のスタンダードとなっているのかを理解し、実際に開発環境を整えて最初のコードを実行するところまでを学びます。 + +## TypeScriptとは? + +TypeScriptは、Microsoftによって開発されているオープンソースのプログラミング言語です。一言で言えば、**「型(Type)を持ったJavaScript」**です。 + +重要な特徴は以下の通りです: + + * **JavaScriptのスーパーセット(上位互換):** すべての有効なJavaScriptコードは、有効なTypeScriptコードでもあります。つまり、今日から既存のJS知識をそのまま活かせます。 + * **静的型付け:** JavaScriptは実行時に変数の型が決まる「動的型付け言語」ですが、TypeScriptはコンパイル時(コードを書いている途中やビルド時)に型をチェックする「静的型付け言語」としての性質を持ちます。 + * **コンパイル(トランスパイル):** ブラウザやNode.jsはTypeScriptを直接理解できません。TypeScriptコンパイラ(`tsc`)を使って、標準的なJavaScriptファイルに変換してから実行します。 + +## なぜTypeScriptか? + +「わざわざ型を書くのは面倒だ」と感じるかもしれません。しかし、中〜大規模な開発においてTypeScriptは以下の強力なメリットを提供します。 + +1. **型安全性(バグの早期発見):** + `undefined` のプロパティを読み取ろうとしたり、数値を期待する関数に文字列を渡したりするミスを、コードを実行する前にエディタ上で警告してくれます。 +2. **強力なエディタサポート:** + VS Codeなどのエディタでは、型情報に基づいた正確なコード補完(IntelliSense)が効きます。APIの仕様をドキュメントで調べなくても、ドット`.`を打つだけで利用可能なメソッドが表示されます。 +3. **リファクタリングの容易さ:** + 変数名や関数名を変更する際、型情報があるおかげで、影響範囲を自動的に特定し、安全に一括置換できます。 + +## 環境構築 + +それでは、実際にTypeScriptを動かす環境を作りましょう。 + +### プロジェクトの作成とTypeScriptのインストール + +今回はローカル環境にTypeScriptをインストールする方法を採用します。適当なディレクトリを作成し、ターミナルで以下のコマンドを実行してください。 + +※あらかじめ [Node.js](https://nodejs.org/) がインストールされていることを前提とします。 + +```bash +# プロジェクトフォルダの作成と移動 +mkdir ts-tutorial +cd ts-tutorial + +# package.jsonの初期化 +npm init -y + +# TypeScriptのインストール(開発用依存関係として) +npm install --save-dev typescript +``` + +インストールが完了したら、バージョンを確認してみましょう。 + +```bash +npx tsc --version +# Output: Version 5.x.x (バージョンは時期によります) +``` + +## 最初のTypeScript + +いよいよ最初のTypeScriptコードを書いてみましょう。 + +### コードの記述 + +エディタで `hello.ts` というファイルを作成し、以下のコードを記述します。 +JavaScriptと似ていますが、変数宣言の後ろに `: string` という「型注釈(Type Annotation)」が付いている点に注目してください。 + +```ts:hello.ts +// 変数messageにstring型(文字列)を指定 +const message: string = "Hello, TypeScript World!"; + +// 数値を渡そうとするとエディタ上でエラーになります(後ほど解説) +console.log(message); +``` + +### コンパイルと実行 + +このままではNode.jsで実行できないため、JavaScriptにコンパイルします。 + +```bash +npx tsc hello.ts +``` + +エラーが出なければ、同じフォルダに `hello.js` というファイルが生成されています。中身を確認すると、型注釈が取り除かれた普通のJavaScriptになっているはずです。 + +生成されたJSファイルをNode.jsで実行します。 + +```ts-exec:hello.ts +Hello, TypeScript World! +``` + +これがTypeScript開発の基本的なサイクル(記述 → コンパイル → 実行)です。 + +このウェブサイトでは上のようにコードを編集して実行ボタンを押すとコンパイルと実行を行うことができる環境を埋め込んでいます。 + +またコンパイル後のjsファイルの内容も以下のように確認できます。 + +```js-readonly:hello.js +"use strict"; +// 変数messageにstring型(文字列)を指定 +const message = "Hello, TypeScript World!"; +// 数値を渡そうとするとエディタ上でエラーになります(後ほど解説) +console.log(message); +``` + +## tsconfig.json: コンパイラの設定 + +毎回 `npx tsc hello.ts` のようにファイル名を指定するのは手間ですし、プロジェクト全体の設定も管理しづらくなります。そこで、`tsconfig.json` という設定ファイルを使用します。 + +以下のコマンドで初期設定ファイルを生成します。 + +```bash +npx tsc --init +``` + +生成された `tsconfig.json` には多くの設定項目がありますが、基本として以下の設定が有効(コメントアウトされていない状態)になっているか確認してください。 + +```json +{ + "compilerOptions": { + "target": "es2016", /* コンパイル後のJSのバージョン */ + "module": "commonjs", /* モジュールシステム */ + "strict": true, /* 厳格な型チェックを有効にする(重要) */ + "esModuleInterop": true, /* CommonJSモジュールとの互換性 */ + "forceConsistentCasingInFileNames": true, /* ファイル名の大文字小文字を区別 */ + "skipLibCheck": true /* 定義ファイルのチェックをスキップ */ + } +} +``` + +### 設定ファイルを使ったコンパイル + +`tsconfig.json` があるディレクトリでは、ファイル名を指定せずに以下のコマンドだけで、ディレクトリ内のすべてのTypeScriptファイルが設定に基づいてコンパイルされます。 + +```bash +npx tsc +``` + +> **Note:** `strict: true` はTypeScriptの恩恵を最大限に受けるために非常に重要です。このチュートリアルでは常にこの設定が有効であることを前提に進めます。 diff --git a/public/docs/typescript-2.md b/public/docs/typescript-2.md new file mode 100644 index 0000000..6eeb2bf --- /dev/null +++ b/public/docs/typescript-2.md @@ -0,0 +1,231 @@ +# 第2章: 基本的な型と型推論 + +JavaScriptでの開発経験がある皆様、TypeScriptの世界へようこそ。 +第1章では環境構築を行いましたが、本章からいよいよ具体的なコードを書いていきます。 + +TypeScriptの最大の武器は**「型(Type)」**です。しかし、すべてのコードに手動で型を書く必要はありません。TypeScriptは非常に賢い「型推論」という機能を持っており、JavaScriptを書く感覚のまま、安全性を享受できる場面も多々あります。 + +この章では、基礎となるプリミティブ型、TypeScriptならではの配列やタプルの扱い、そして「何でもあり」な状態をどう制御するかについて学びます。 + +## 2.1 型注釈の構文 (Type Annotations) + +変数を宣言する際、その変数がどのような種類のデータを扱うかを明示することを「型注釈(Type Annotation)」と呼びます。 +構文は非常にシンプルで、変数名の後ろに `: 型名` を記述します。 + +```ts:annotation.ts +// 文字列型の変数を宣言 +let message: string = "Hello, TypeScript!"; + +// 数値型の定数を宣言 +const userId: number = 1001; + +// コンソールに出力 +console.log(message); +console.log(`User ID: ${userId}`); + +// エラーになる例(コメントアウトを外すとエディタ上で赤線が出ます) +// message = 123; // Error: Type 'number' is not assignable to type 'string'. +``` + +```ts-exec:annotation.ts +Hello, TypeScript! +User ID: 1001 +``` +```js-readonly:annotation.js +``` + +> **ポイント:** JavaScriptでは変数にどんな値でも再代入できましたが、TypeScriptでは宣言された型と異なる値を代入しようとすると、コンパイルエラー(またはエディタ上の警告)が発生します。これがバグを未然に防ぐ第一の砦です。 + +## 2.2 主要なプリミティブ型 + +JavaScriptでおなじみのプリミティブ型は、TypeScriptでもそのまま使用できます。 + + * **string**: 文字列 (`"hello"`, `'world'`, \`template\`) + * **number**: 数値 (整数、浮動小数点数、`NaN`, `Infinity` すべて含む) + * **boolean**: 真偽値 (`true`, `false`) + +注意点として、`Number`や`String`(大文字始まり)はラッパーオブジェクト型を指すため、通常は**小文字**の`number`, `string`を使用してください。 + +```ts:primitives.ts +let isDone: boolean = false; +let decimal: number = 6; +let hex: number = 0xf00d; +let color: string = "blue"; + +// テンプレートリテラルもstring型として扱われます +let summary: string = `Color is ${color} and Hex is ${hex}`; + +console.log("Is Done:", isDone); +console.log(summary); +``` + +```ts-exec:primitives.ts +Is Done: false +Color is blue and Hex is 61453 +``` +```js-readonly:primitives.js +``` + +## 2.3 型推論 (Type Inference) + +ここがJavaScript経験者にとって嬉しいポイントです。 +変数の初期化と同時に値を代入する場合、**型注釈を省略してもTypeScriptが自動的に型を判別**してくれます。これを「型推論」と呼びます。 + +```ts:inference.ts +// 型注釈がないが、"TypeScript"という文字列から string型 と推論される +let techName = "TypeScript"; + +// 数値が入っているため、count は number型 と推論される +let count = 42; + +console.log(`Technology: ${techName}, Count: ${count}`); + +// 推論された型と違う値を入れようとするとエラーになる +// count = "Forty-Two"; // Error! +``` + +```ts-exec:inference.ts +Technology: TypeScript, Count: 42 +``` +```js-readonly:inference.js +``` + +> **ベストプラクティス:** 初期値がある場合、わざわざ `: string` などを書く必要はありません。コードが冗長になるのを防ぐため、明示的な型注釈は「初期値がない場合」や「推論される型とは別の型として扱いたい場合」に使用するのが一般的です。 + +## 2.4 特殊な型: any, unknown, never + +TypeScriptには「特定のデータ型」ではない特殊な型が存在します。これらは安全性に大きく関わるため、違いを理解することが重要です。 + +### any: 危険な「何でもあり」 + +`any` 型は、型チェックを無効にする型です。JavaScriptと同じ挙動になりますが、TypeScriptを使うメリットが失われるため、**可能な限り使用を避けてください**。 + +### unknown: 安全な「正体不明」 + +「何が入ってくるかわからない」場合(例:外部APIのレスポンスなど)は、`any`の代わりに`unknown`を使います。`unknown`型の変数は、**「型の絞り込み(Type Narrowing)」を行わない限り、プロパティへのアクセスやメソッドの呼び出しができません**。 + +### never: 決して発生しない + +`never` は「値を持たない」ことを意味します。常に例外を投げる関数や、無限ループなど「終了しない関数」の戻り値として使われます。 + +```ts:special_types.ts +// --- any の例 --- +let looseVariable: any = 4; +looseVariable = "Maybe a string instead"; +looseVariable = false; // エラーにならない(危険!) +console.log("Any:", looseVariable); + +// --- unknown の例 --- +let uncertainValue: unknown = "I am actually a string"; + +// uncertainValue.toUpperCase(); // エラー: Object is of type 'unknown'. + +// 型チェック(絞り込み)を行うと使用可能になる +if (typeof uncertainValue === "string") { + console.log("Unknown (checked):", uncertainValue.toUpperCase()); +} + +// --- never の例 --- +function throwError(message: string): never { + throw new Error(message); +} + +try { + // この関数は決して正常に戻らない + throwError("Something went wrong"); +} catch (e) { + console.log("Error caught"); +} +``` + +```ts-exec:special_types.ts +Any: false +Unknown (checked): I AM ACTUALLY A STRING +Error caught +``` +```js-readonly:special_types.js +``` + +## 2.5 配列とタプル + +データの集合を扱う方法を見ていきましょう。 + +### 配列 (Array) + +配列の型定義には2通りの書き方があります。 + +1. `型[]` (推奨:シンプル) +2. `Array<型>` (ジェネリクス記法) + +### タプル (Tuple) + +配列に似ていますが、**「要素の数が固定」**で、**「各要素の型が決まっている」**ものをタプルと呼びます。CSVの1行や、座標`(x, y)`などを表現するのに便利です。 + +```ts:arrays_tuples.ts +// --- 配列 --- +// 数値の配列 +let fibonacci: number[] = [1, 1, 2, 3, 5]; + +// 文字列の配列(Array記法) +let frameworkList: Array = ["React", "Vue", "Angular"]; + +// --- タプル --- +// [名前, 年齢, 有効フラグ] の順序と型を守る必要がある +let userTuple: [string, number, boolean]; + +userTuple = ["Alice", 30, true]; +// userTuple = [30, "Alice", true]; // エラー: 型の順序が違う + +console.log("First Framework:", frameworkList[0]); +console.log(`User: ${userTuple[0]}, Age: ${userTuple[1]}`); + +// fibonacci.push("8"); // エラー: number[] に string は追加できない +fibonacci.push(8); // OK +console.log("Next Fib:", fibonacci[fibonacci.length - 1]); +``` + +```ts-exec:arrays_tuples.ts +First Framework: React +User: Alice, Age: 30 +Next Fib: 8 +``` +```js-readonly:arrays_tuples.js +``` + +## この章のまとめ + + * 変数宣言時に `: 型名` で型注釈をつけることができる。 + * 初期値がある場合、TypeScriptは自動的に型を推測する(**型推論**)。 + * プリミティブ型は `string`, `number`, `boolean` を小文字で使う。 + * `any` は型チェックを無効にするため避け、不明な値には `unknown` を使う。 + * **配列**は同じ型の集まり、**タプル**は位置と型が固定された配列である。 + +次回は、より複雑なデータ構造を扱うための「オブジェクト、インターフェース、型エイリアス」について学びます。 + +### 練習問題 1: タプルと配列の操作 + +1. 「商品名(string)」と「価格(number)」を持つ**タプル**型の変数 `product` を定義し、`["Keyboard", 5000]` を代入してください。 +2. 文字列の**配列** `tags` を定義し、型推論を使って `["IT", "Gadget"]` で初期化してください。 +3. `tags` に新しいタグ `"Sale"` を追加してください。 +4. それぞれの値をコンソールに出力してください。 + +```ts:practice2_1.ts +``` +```ts-exec:practice2_1.ts +``` +```js-readonly:practice2_1.js +``` + +### 練習問題 2: unknown型の安全な利用 + +1. `unknown` 型の引数 `input` を受け取る関数 `printLength` を作成してください。 +2. 関数内で、`input` が `string` 型である場合のみ、その文字列の長さをコンソールに出力してください(`input.length`)。 +3. `input` が `string` 以外の場合は、「Not a string」と出力してください。 +4. この関数に 文字列 `"TypeScript"` と 数値 `100` を渡して実行してください。 + +```ts:practice2_2.ts +``` +```ts-exec:practice2_2.ts +``` +```js-readonly:practice2_2.js +``` diff --git a/public/docs/typescript-3.md b/public/docs/typescript-3.md new file mode 100644 index 0000000..4d9e361 --- /dev/null +++ b/public/docs/typescript-3.md @@ -0,0 +1,221 @@ +# 第3章: オブジェクト、インターフェース、型エイリアス + +JavaScriptでは、オブジェクトはデータを扱うための中心的な存在です。TypeScriptにおいてもそれは変わりませんが、JavaScriptの自由度に「型」という制約を加えることで、開発時の安全性を劇的に高めることができます。 + +この章では、オブジェクトの形状(Shape)を定義するための主要な方法である**型エイリアス(type)**と**インターフェース(interface)**について学びます。 + +## オブジェクトの型付け: インラインでの定義 + +最も基本的な方法は、変数宣言時に直接オブジェクトの構造(型)を記述する方法です。これを「インラインの型定義」や「オブジェクトリテラル型」と呼びます。 + +```ts:inline-object.ts +// 変数名の後ろに : { プロパティ名: 型; ... } を記述します +const book: { title: string; price: number; isPublished: boolean } = { + title: "TypeScript入門", + price: 2500, + isPublished: true, +}; + +console.log(`Title: ${book.title}, Price: ${book.price}`); +``` + +```ts-exec:inline-object.ts +Title: TypeScript入門, Price: 2500 +``` +```js-readonly:inline-object.js +``` + +この方法はシンプルですが、同じ構造を持つオブジェクトを複数作成する場合、毎回型定義を書く必要があり、コードが冗長になります。そこで登場するのが「型に名前を付ける」機能です。 + +## 型エイリアス (type): 型に名前を付ける + +**型エイリアス(Type Alias)**を使用すると、特定の型定義に名前を付け、それを再利用することができます。JavaScriptの経験がある方にとって、これは「型の変数」を作るようなものだとイメージしてください。 + +キーワードは `type` です。慣習として型名には **PascalCase**(大文字始まり)を使用します。 + +```ts:type-alias.ts +// User型を定義 +type User = { + name: string; + age: number; + email: string; +}; + +// 定義したUser型を使用 +const user1: User = { + name: "Tanaka", + age: 28, + email: "tanaka@example.com", +}; + +const user2: User = { + name: "Suzuki", + age: 34, + email: "suzuki@example.com", +}; + +// 関数の引数としても利用可能 +function greet(user: User): string { + return `Hello, ${user.name}!`; +} + +console.log(greet(user1)); +``` + +```ts-exec:type-alias.ts +Hello, Tanaka! +``` + +```js-readonly:type-alias.js +``` + +## インターフェース (interface): オブジェクトの「形状」を定義する + +オブジェクトの構造を定義するもう一つの代表的な方法が **インターフェース(interface)** です。 +JavaやC\#などの言語経験がある方には馴染み深いキーワードですが、TypeScriptのインターフェースは「クラスのための契約」だけでなく、「純粋なオブジェクトの形状定義」としても頻繁に使用されます。 + +```ts:interface-basic.ts +// interfaceキーワードを使用(= は不要) +interface Car { + maker: string; + model: string; + year: number; +} + +const myCar: Car = { + maker: "Toyota", + model: "Prius", + year: 2023, +}; + +console.log(`${myCar.maker} ${myCar.model} (${myCar.year})`); +``` + +```ts-exec:interface-basic.ts +Toyota Prius (2023) +``` +```js-readonly:interface-basic.js +``` + +## type vs interface: 使い分けの基本的なガイドライン + +「`type` と `interface` のどちらを使うべきか?」は、TypeScriptにおける最大の論点の一つです。 +現在のTypeScriptでは機能的な差は非常に少なくなっていますが、基本的な使い分けのガイドラインは以下の通りです。 + +| 特徴 | type (型エイリアス) | interface (インターフェース) | +| :--- | :--- | :--- | +| **主な用途** | プリミティブ、ユニオン型(第5章で解説)、タプル、関数の型など、**あらゆる型**に名前を付ける。 | **オブジェクトの構造**やクラスの実装ルールを定義する。 | +| **拡張性** | 交差型 (`&`) を使って拡張する。 | `extends` キーワードで継承できる。また、同名のinterfaceを定義すると自動でマージされる(Declaration Merging)。 | +| **推奨シーン** | アプリケーション開発全般、複雑な型の組み合わせ。 | ライブラリ開発(拡張性を残すため)、オブジェクト指向的な設計。 | + +**結論としての指針:** +初心者のうちは、**「オブジェクトの定義には `interface`、それ以外(単純な型や複雑な型の合成)には `type`」** というルールで始めるのが無難です。 +あるいは、最近のトレンドとして「一貫して `type` を使う」というチームも増えています。重要なのは**プロジェクト内で統一すること**です。 + +## オプショナルなプロパティ (?) + +オブジェクトによっては、特定のプロパティが存在しない(省略可能である)場合があります。 +プロパティ名の後ろに `?` を付けることで、そのプロパティを **オプショナル(任意)** に設定できます。 + +```ts:optional-properties.ts +interface UserProfile { + username: string; + avatarUrl?: string; // ? があるので、このプロパティはなくてもエラーにならない +} + +const profileA: UserProfile = { + username: "user_a", + avatarUrl: "https://example.com/a.png", +}; + +const profileB: UserProfile = { + username: "user_b", + // avatarUrl は省略可能 +}; + +console.log(profileA); +console.log(profileB); +``` + +```ts-exec:optional-properties.ts +{ username: 'user_a', avatarUrl: 'https://example.com/a.png' } +{ username: 'user_b' } +``` +```js-readonly:optional-properties.js +``` + +この場合、`avatarUrl` の型は実質的に `string | undefined`(文字列 または undefined)として扱われます。 + +## 読み取り専用プロパティ (readonly) + +オブジェクトのプロパティを初期化した後に変更されたくない場合、`readonly` 修飾子を使用します。これは特に、IDや設定値など、不変であるべきデータを扱う際に有用です。 + +```ts:readonly-properties.ts +type Product = { + readonly id: number; // 書き換え不可 + name: string; // 書き換え可能 + price: number; +}; + +const item: Product = { + id: 101, + name: "Laptop", + price: 98000 +}; + +item.price = 95000; // OK: 通常のプロパティは変更可能 + +// 以下の行はコンパイルエラーになります +// item.id = 102; // Error: Cannot assign to 'id' because it is a read-only property. + +console.log(item); +``` + +```ts-exec:readonly-properties.ts +{ id: 101, name: 'Laptop', price: 95000 } +``` +```js-readonly:readonly-properties.js +``` + +注意点として、`readonly` はあくまで TypeScript のコンパイル時のチェックです。実行時の JavaScript コードでは通常のオブジェクトとして振る舞うため、無理やり書き換えるコードが混入すると防げない場合があります(ただし、TSを使っている限りはその前にエラーで気づけます)。 + +## この章のまとめ + + * **インライン定義**: `{ key: type }` でその場限りの型定義が可能。 + * **型エイリアス (`type`)**: 型定義に名前を付けて再利用しやすくする。柔軟性が高い。 + * **インターフェース (`interface`)**: オブジェクトの構造を定義することに特化している。 + * **オプショナル (`?`)**: プロパティを必須ではなく任意にする。 + * **読み取り専用 (`readonly`)**: プロパティの再代入を禁止し、不変性を保つ。 + +### 練習問題 1: 商品在庫管理 + +以下の条件を満たす `Item` インターフェースを定義し、そのオブジェクトを作成してください。 + +1. `id` は数値で、読み取り専用 (`readonly`) であること。 +2. `name` は文字列であること。 +3. `price` は数値であること。 +4. `description` は文字列だが、省略可能 (`?`) であること。 +5. 作成したオブジェクトの `price` を変更し、コンソールに出力してください。 + +```ts:practice3_1.ts +``` +```ts-exec:practice3_1.ts +``` +```js-readonly:practice3_1.js +``` + +### 練習問題 2: ユーザー情報の統合 + +以下の2つの型エイリアスを定義してください。 + +1. `Contact`: `email` (string) と `phone` (string) を持つ。 +2. `Employee`: `id` (number), `name` (string), `contact` (`Contact`型) を持つ。 + * つまり、`Employee` の中に `Contact` 型がネスト(入れ子)されている状態です。 +3. この `Employee` 型を使って、あなたの情報を表現する変数を作成してください。 + +```ts:practice3_2.ts +``` +```ts-exec:practice3_2.ts +``` +```js-readonly:practice3_2.js +``` diff --git a/public/docs/typescript-4.md b/public/docs/typescript-4.md new file mode 100644 index 0000000..7c8a993 --- /dev/null +++ b/public/docs/typescript-4.md @@ -0,0 +1,279 @@ +# 第4章: 関数の型定義 + +JavaScript開発者にとって、関数はロジックの中心的な構成要素です。JavaScriptでは引数の数や型が柔軟(あるいはルーズ)ですが、TypeScriptではここを厳密に管理することで、実行時エラーの大半を防ぐことができます。 + +この章では、TypeScriptにおける関数の型定義の基本から、モダンなJavaScript開発で必須となるアロー関数、そして高度なオーバーロードまでを学習します。 + +## 引数と戻り値の型 + +TypeScriptの関数定義において最も基本的なルールは、「引数」と「戻り値」に型を付けることです。 + + * **引数**: 変数名の後ろに `: 型` を記述します。 + * **戻り値**: 引数リストの閉じ括弧 `)` の後ろに `: 型` を記述します。 + +戻り値の型は型推論(Chapter 2参照)によって省略可能ですが、関数の意図を明確にするために明示的に書くことが推奨されます。戻り値がない場合は `void` を使用します。 + +```ts:basic_math.ts +// 基本的な関数宣言 +function add(a: number, b: number): number { + return a + b; +} + +// 戻り値がない関数 +function logMessage(message: string): void { + console.log(`LOG: ${message}`); +} + +const result = add(10, 5); +logMessage(`Result is ${result}`); + +// エラー例(コメントアウトを外すとエラーになります) +// add(10, "5"); // Error: Argument of type 'string' is not assignable to parameter of type 'number'. +``` + +```ts-exec:basic_math.ts +LOG: Result is 15 +``` +```js-readonly:basic_math.js +``` + +## オプショナル引数とデフォルト引数 + +JavaScriptでは引数を省略すると `undefined` になりますが、TypeScriptでは定義された引数は**必須**とみなされます。引数を省略可能にするには、特別な構文が必要です。 + +### オプショナル引数 (`?`) + +引数名の後ろに `?` を付けることで、その引数を省略可能(オプショナル)にできます。省略された場合の値は `undefined` です。 + +> **注意:** オプショナル引数は、必ず必須引数の**後ろ**に配置する必要があります。 + +### デフォルト引数 (`=`) + +ES6(JavaScript)と同様に、引数にデフォルト値を指定できます。デフォルト値がある場合、TypeScriptはその引数を「型推論」し、かつ「省略可能」として扱います。 + +```ts:optional_default.ts +// titleは省略可能 +function greet(name: string, title?: string): string { + if (title) { + return `Hello, ${title} ${name}!`; + } + return `Hello, ${name}!`; +} + +// powerのデフォルト値は2 +// 戻り値の型はnumberと推論されるため省略可能 +function exponent(base: number, power: number = 2) { + return base ** power; +} + +console.log(greet("Tanaka")); +console.log(greet("Sato", "Dr.")); +console.log(`2^2 = ${exponent(2)}`); +console.log(`2^3 = ${exponent(2, 3)}`); +``` + +```ts-exec:optional_default.ts +Hello, Tanaka! +Hello, Dr. Sato! +2^2 = 4 +2^3 = 8 +``` +```js-readonly:optional_default.js +``` + +## アロー関数と `this` + +### アロー関数の型定義 + +アロー関数を変数に代入する場合、引数と戻り値の記述場所は通常の関数と同様です。 + +```ts:arrow_func.ts +const multiply = (x: number, y: number): number => { + return x * y; +}; + +// 1行で書く場合(暗黙のreturn) +const subtract = (x: number, y: number): number => x - y; + +console.log(multiply(4, 5)); +console.log(subtract(10, 3)); +``` + +```ts-exec:arrow_func.ts +20 +7 +``` +```js-readonly:arrow_func.js +``` + +### `this` の型指定 + +JavaScriptにおいて `this` の挙動は複雑ですが、TypeScriptでは `this` が何を指すかを明示的に型定義できます。 +これを行うには、関数の**最初の引数**として `this` という名前の「偽の引数」を定義します。これはコンパイル後のJavaScriptには出力されません。 + +```ts:this_context.ts +interface User { + name: string; + count: number; +} + +function counter(this: User) { + this.count += 1; + console.log(`${this.name}: ${this.count}`); +} + +const userA: User = { name: "Alice", count: 0 }; + +// callメソッドを使ってthisコンテキストを指定して実行 +counter.call(userA); +counter.call(userA); + +// アロー関数はthisを持たないため、この構文は使いません +``` + +```ts-exec:this_context.ts +Alice: 1 +Alice: 2 +``` +```js-readonly:this_context.js +``` + +## 関数のオーバーロード + +JavaScriptでは「引数の型や数によって挙動が変わる関数」をよく書きます。TypeScriptでこれを表現するには**オーバーロード**を使用します。 + +オーバーロードは以下の2つの部分で構成されます: + +1. **オーバーロードシグネチャ**: 関数の呼び出しパターンを定義(複数可)。実装は書きません。 +2. **実装シグネチャ**: 実際の関数の処理。外部からは直接見えません。 + +```ts:overload.ts +// 1. オーバーロードシグネチャ(呼び出し可能なパターン) +function double(value: number): number; +function double(value: string): string; + +// 2. 実装シグネチャ(すべてのパターンを網羅できる型定義にする) +function double(value: number | string): number | string { + if (typeof value === 'number') { + return value * 2; + } else { + return value.repeat(2); + } +} + +const numResult = double(10); // 型は number として推論される +const strResult = double("Hi"); // 型は string として推論される + +console.log(numResult); +console.log(strResult); + +// double(true); // エラー: booleanを受け入れるオーバーロードはありません +``` + +```ts-exec:overload.ts +20 +HiHi +``` +```js-readonly:overload.js +``` + + +> **ポイント:** 実装シグネチャ(`number | string` の部分)は直接呼び出せません。必ず上で定義したシグネチャ(`number` または `string`)に一致する必要があります。 + +## 残余引数 (Rest Parameters) + +引数の数が可変である場合(可変長引数)、JavaScriptと同様に `...args` 構文を使用します。 +TypeScriptでは、この `args` は必ず**配列の型**である必要があります。 + +```ts:rest_params.ts +// 数値を好きなだけ受け取り、合計を返す +function sumAll(...numbers: number[]): number { + return numbers.reduce((total, num) => total + num, 0); +} + +// 文字列を結合する +function joinStrings(separator: string, ...words: string[]): string { + return words.join(separator); +} + +console.log(sumAll(1, 2, 3, 4, 5)); +console.log(joinStrings("-", "TypeScript", "is", "fun")); +``` + +```ts-exec:rest_params.ts +15 +TypeScript-is-fun +``` +```js-readonly:rest_params.js +``` + +## 関数の型エイリアス + +コールバック関数を引数に取る場合など、関数の型定義が長くなりがちです。 +第3章で学んだ `type`(型エイリアス)を使って、関数のシグネチャそのものに名前を付けることができます。 + +構文: `type 型名 = (引数: 型) => 戻り値の型;` + +```ts:func_alias.ts +// 関数の型定義を作成 +type MathOperation = (x: number, y: number) => number; + +// 作成した型を適用 +const addition: MathOperation = (a, b) => a + b; +const multiplication: MathOperation = (a, b) => a * b; + +// 高階関数での利用例(関数を受け取る関数) +function compute(x: number, y: number, op: MathOperation): number { + return op(x, y); +} + +console.log(compute(10, 2, addition)); +console.log(compute(10, 2, multiplication)); +``` + +```ts-exec:func_alias.ts +12 +20 +``` +```js-readonly:func_alias.js +``` + + +## この章のまとめ + + * 関数定義では、引数と戻り値に型を明記するのが基本です。 + * `?` でオプショナル引数、`=` でデフォルト引数を定義できます。 + * アロー関数や `this` の型付けもサポートされており、コンテキストミスを防げます。 + * **オーバーロード**を使うことで、引数によって戻り値の型が変わる柔軟な関数を定義できます。 + * **型エイリアス**を使うことで、複雑な関数シグネチャを再利用可能なパーツとして定義できます。 + +### 練習問題 1: ユーザー検索関数 + +以下の要件を満たす `findUser` 関数をアロー関数として作成してください。 + +1. 引数 `id` (number) と `name` (string) を受け取る。 +2. `name` はオプショナル引数とする。 +3. 戻り値は「検索中: [id] [name]」という文字列(nameがない場合は「検索中: [id] ゲスト」)とする。 +4. 関数の型定義(Type Alias)を `SearchFunc` として先に定義し、それを適用すること。 + +```ts:practice4_1.ts +``` +```ts-exec:practice4_1.ts +``` +```js-readonly:practice4_1.js +``` + +### 練習問題 2: データ変換のオーバーロード + +以下の要件を満たす `convert` 関数を `function` キーワードで作成してください。 + +1. 引数が `number` の場合、それを `string` に変換して返す(例: `100` -\> `"100"`)。 +2. 引数が `string` の場合、それを `number` に変換して返す(例: `"100"` -\> `100`)。 +3. 適切なオーバーロードシグネチャを2つ定義すること。 + +```ts:practice4_2.ts +``` +```ts-exec:practice4_2.ts +``` +```js-readonly:practice4_2.js +``` \ No newline at end of file diff --git a/public/docs/typescript-5.md b/public/docs/typescript-5.md new file mode 100644 index 0000000..8d4fd62 --- /dev/null +++ b/public/docs/typescript-5.md @@ -0,0 +1,299 @@ +# 第5章: 型を組み合わせる + +これまでの章では、`string` や `number`、あるいは特定のオブジェクトの形といった「単一の型」を扱ってきました。しかし、現実のアプリケーション開発(特にJavaScriptの世界)では、「IDは数値かもしれないし、文字列かもしれない」「成功時はデータを返すが、失敗時はエラーメッセージを返す」といった柔軟なデータ構造が頻繁に登場します。 + +この章では、既存の型をパズルのように組み合わせて、より複雑で柔軟な状況を表現する方法を学びます。 + +## Union型 (共用体型) + +Union型(共用体型)は、**「A または B」**という状態を表現します。パイプ記号 `|` を使用して記述します。 + +JavaScriptでは変数の型が動的であるため、1つの変数に異なる型の値が入ることがよくありますが、TypeScriptではUnion型を使ってこれを安全に定義できます。 + +```ts:union-basic.ts +// idは数値、または文字列を許容する +let id: number | string; + +id = 101; // OK +id = "user-a"; // OK +// id = true; // Error: Type 'boolean' is not assignable to type 'string | number'. + +function printId(id: number | string) { + console.log(`Your ID is: ${id}`); +} + +printId(123); +printId("ABC"); +``` + +```ts-exec:union-basic.ts +Your ID is: 123 +Your ID is: ABC +``` +```js-readonly:union-basic.js +``` + +> **注意点:** Union型を使用している変数は、その時点では「どの型か確定していない」ため、**すべての候補に共通するプロパティやメソッド**しか操作できません。特定の型として扱いたい場合は、後述する「型ガード」を使用します。 + +## Literal型 (リテラル型) + +`string` や `number` は「あらゆる文字列」や「あらゆる数値」を受け入れますが、**「特定の値だけ」**を許可したい場合があります。これをLiteral型(リテラル型)と呼びます。 + +通常、Literal型は単独で使うよりも、Union型と組み合わせて**「決まった選択肢のいずれか」**を表現するのによく使われます(Enumの代わりとしてもよく利用されます)。 + +```ts:literal-types.ts +// 文字列リテラル型とUnion型の組み合わせ +type TrafficLight = 'red' | 'yellow' | 'green'; + +let currentLight: TrafficLight = 'red'; + +// currentLight = 'blue'; // Error: Type '"blue"' is not assignable to type 'TrafficLight'. + +// 数値リテラルも可能 +type DiceRoll = 1 | 2 | 3 | 4 | 5 | 6; +let dice: DiceRoll = 3; + +console.log(`Light: ${currentLight}, Dice: ${dice}`); +``` + +```ts-exec:literal-types.ts +Light: red, Dice: 3 +``` +```js-readonly:literal-types.js +``` + +## Intersection型 (交差型) + +Intersection型(交差型)は、**「A かつ B」**を表します。アンパサンド `&` を使用します。 +これは主にオブジェクトの型定義を合成(マージ)して、**「複数の型のすべてのプロパティを持つ新しい型」**を作る際によく使用されます。 + +```ts:intersection-types.ts +type Person = { + name: string; +}; + +type Employee = { + employeeId: number; + department: string; +}; + +// Person かつ Employee の特徴を持つ型 +type CompanyMember = Person & Employee; + +const member: CompanyMember = { + name: "Suzuki", + employeeId: 5001, + department: "Engineering" + // どれか一つでも欠けるとエラーになります +}; + +console.log(member); +``` + +```ts-exec:intersection-types.ts +{ name: 'Suzuki', employeeId: 5001, department: 'Engineering' } +``` +```js-readonly:intersection-types.js +``` + +> **補足:** プリミティブ型同士で `string & number` のように交差させると、両方を満たす値は存在しないため、型は `never`(ありえない値)になります。Intersection型は主にオブジェクト型の合成に使われます。 + +## null と undefined + +TypeScriptには `null` 型と `undefined` 型が存在します。 +`tsconfig.json` の設定で `strictNullChecks: true`(推奨設定)になっている場合、これらは他の型(stringなど)には代入できません。 + +値が存在しない可能性がある場合は、Union型を使って明示的に `null` や `undefined` を許可します。 + +```ts:nullable.ts +// string または null を許容する +let userName: string | null = "Tanaka"; + +userName = null; // OK + +// オプショナルなプロパティ(?)は 「型 | undefined」 の糖衣構文に近い動きをします +type UserConfig = { + theme: string; + notification?: boolean; // boolean | undefined +}; + +const config: UserConfig = { + theme: "dark" + // notification は省略可能 (undefined) +}; + +console.log(`User: ${userName}, Theme: ${config.theme}`); +``` + +```ts-exec:nullable.ts +User: null, Theme: dark +``` +```js-readonly:nullable.js +``` + +## 型ガード (Type Guards) + +Union型 (`string | number`) の変数があるとき、プログラムの中で「今は `string` なのか `number` なのか」を区別して処理を分けたい場合があります。これを**型の絞り込み(Narrowing)**と言います。 + +TypeScriptのコンパイラが「このブロック内ではこの変数はこの型だ」と認識できるようにするチェック処理を**型ガード**と呼びます。 + +### typeof 演算子 + +プリミティブ型(string, number, boolean, symbol, undefined)の判定に使います。 + +```ts:type-guard-typeof.ts +function formatPrice(price: number | string) { + // ここでは price は number | string + + if (typeof price === 'string') { + // このブロック内では price は 'string' 型として扱われる + return parseInt(price).toLocaleString(); + } else { + // このブロック内では price は 'number' 型として扱われる + return price.toLocaleString(); + } +} + +console.log(formatPrice(10000)); +console.log(formatPrice("20000")); +``` + +```ts-exec:type-guard-typeof.ts +10,000 +20,000 +``` +```js-readonly:type-guard-typeof.js +``` + +### in 演算子 + +オブジェクトが特定のプロパティを持っているかどうかで型を絞り込みます。 + +```ts:type-guard-in.ts +type Fish = { swim: () => void }; +type Bird = { fly: () => void }; + +function move(animal: Fish | Bird) { + if ('swim' in animal) { + // ここでは Fish 型 + animal.swim(); + } else { + // ここでは Bird 型 + animal.fly(); + } +} + +const fish: Fish = { swim: () => console.log("Swimming...") }; +move(fish); +``` + +```ts-exec:type-guard-in.ts +Swimming... +``` +```js-readonly:type-guard-in.js +``` + +### instanceof 演算子 + +クラスのインスタンスかどうかを判定します(第7章のクラスで詳しく扱いますが、Dateなどの組み込みオブジェクトでも有効です)。 + +```ts:type-guard-instanceof.ts +function logDate(value: string | Date) { + if (value instanceof Date) { + console.log(value.toISOString()); + } else { + console.log(value); + } +} +``` +```js-readonly:type-guard-instanceof.js +``` + +## 型アサーション (Type Assertions) + +時に、プログラマがTypeScriptコンパイラよりも型の詳細を知っている場合があります。例えば、外部APIからのレスポンスや、DOM要素の取得などです。 + +`as` キーワードを使うと、コンパイラに対して「この変数はこの型であるとして扱ってくれ」と強制できます。 + +```ts:assertion.ts +// unknown型は何でも入るが、そのままでは操作できない型 +let someValue: unknown = "This is a string"; + +// コンパイラに「これはstringだからlengthを使わせて」と伝える +let strLength: number = (someValue as string).length; + +console.log(strLength); + +// 注意: 全く互換性のない型への変換はエラーになりますが、 +// unknownを経由すると無理やり変換できてしまうため、乱用は避けてください。 +// let wrong = (123 as string); // Error +// let dangerous = (123 as unknown as string); // OKだが実行時にバグの元 +``` + +```ts-exec:assertion.ts +16 +``` +```js-readonly:assertion.js +``` + +> **注意:** 型アサーションはあくまで「コンパイル時の型チェックを黙らせる」機能であり、実行時の型変換を行うわけではありません。実行時に値が想定と違う場合、クラッシュの原因になります。可能な限り、型ガードを使って安全に絞り込むことを推奨します。 + +## この章のまとめ + + * **Union型 (`|`)**: 複数の型のうち「いずれか」を表す。 + * **Literal型**: 特定の値のみを許容する型。Union型と組み合わせて列挙型のように使える。 + * **Intersection型 (`&`)**: 複数の型を「合成」して、すべてのプロパティを持つ型を作る。 + * **null / undefined**: `strictNullChecks` 環境下では、Union型を使って明示的に許容する必要がある。 + * **型ガード**: `typeof`, `in`, `instanceof` などを使って、Union型から特定の型へ絞り込む。 + * **型アサーション (`as`)**: 型を強制的に指定するが、安全性のために使用は慎重に行う。 + +### 練習問題1: 結果の型定義 + +APIリクエストの結果を表す `Result` 型を定義してください。 + + * 成功時は `success: true` と `data: string` を持ちます。 + * 失敗時は `success: false` と `error: string` を持ちます。 + * `handleResult` 関数内で型ガードを使い、成功ならデータを、失敗ならエラーメッセージをログ出力してください。 + +```ts:practice5_1.ts +// ここに SuccessResult, FailureResult, Result 型を定義してください +// type Result = ... + +function handleResult(result: Result) { + // ここに処理を実装してください +} + +// テスト用 +handleResult({ success: true, data: "Data loaded" }); +handleResult({ success: false, error: "Network error" }); +``` +```ts-exec:practice5_1.ts +``` +```js-readonly:practice5_1.js +``` + + +### 練習問題2: 図形の面積計算 + +`Circle` 型と `Square` 型を定義し、それらのUnion型である `Shape` を定義してください。 + + * `Circle` は `kind: 'circle'` と `radius: number` を持ちます。 + * `Square` は `kind: 'square'` と `sideLength: number` を持ちます。 + * `getArea` 関数で、渡された図形に応じて面積を計算して返してください(円周率は `Math.PI` を使用)。 + +```ts:practice5_2.ts +// ここに型を定義 + +function getArea(shape: Shape): number { + // ここに実装 (switch文やif文で kind プロパティによる絞り込みを行う) + return 0; +} + +// テスト用 +console.log(getArea({ kind: 'circle', radius: 10 })); +console.log(getArea({ kind: 'square', sideLength: 5 })); +``` +```ts-exec:practice5_2.ts +``` +```js-readonly:practice5_2.js +``` diff --git a/public/docs/typescript-6.md b/public/docs/typescript-6.md new file mode 100644 index 0000000..955ce5f --- /dev/null +++ b/public/docs/typescript-6.md @@ -0,0 +1,272 @@ +# 第6章: ジェネリクス (Generics) + +第6章では、TypeScriptを使いこなす上で非常に強力な機能である**ジェネリクス (Generics)** について学びます。JavaやC\#などの言語経験がある方には馴染み深い概念かもしれませんが、JavaScriptの世界から来た方にとっては少し抽象的に感じるかもしれません。しかし、これを理解することで、**「柔軟性」と「安全性」を両立したコード**が書けるようになります。 + +## Genericsの必要性: 型を引数のように扱う + +プログラミングをしていると、「処理内容は同じだが、扱うデータの型だけが違う」という場面によく遭遇します。 + +例えば、「引数をそのまま返す関数」を考えてみましょう。 + +```ts +// 数値を受け取って数値を返す +function returnNumber(arg: number): number { + return arg; +} + +// 文字列を受け取って文字列を返す +function returnString(arg: string): string { + return arg; +} + +// どんな型でも受け取れるが、戻り値の型情報が失われる(any) +function returnAny(arg: any): any { + return arg; +} +``` + +`returnNumber` と `returnString` はロジックが完全に重複しています。一方、`returnAny` は重複を防げますが、TypeScriptの利点である型チェックが無効になってしまいます。 + +ここで登場するのが **ジェネリクス** です。ジェネリクスを使うと、**「型そのもの」を引数のように受け取る**ことができます。 + +## Generics関数 + +ジェネリクスを使った関数の定義を見てみましょう。 +型変数は慣習として `T` (Typeの頭文字) がよく使われます。 + +```ts:identity_func.ts +// は「この関数内で T という名前の型変数を使います」という宣言 +function identity(arg: T): T { + console.log(`引数の型: ${typeof arg}, 値: ${arg}`); + return arg; +} + +// 使用例1: 明示的に型を指定する +const output1 = identity("Hello Generics"); + +// 使用例2: 型推論に任せる (推奨) +// 引数が数値なので、T は number に自動的に推論される +const output2 = identity(100); + +// output1は string型、output2は number型 として扱われるため安全 +// output1.toFixed(2); // エラー: string型にtoFixedは存在しない +``` + +```ts-exec:identity_func.ts +引数の型: string, 値: Hello Generics +引数の型: number, 値: 100 +``` +```js-readonly:identity_func.js +``` + +このように、`identity` 関数は定義時点では型を固定せず、**呼び出す瞬間に型が決まる**という柔軟な性質を持ちます。 + +## Genericsインターフェース + +関数だけでなく、インターフェースもジェネリクスにできます。これにより、再利用性の高いデータ構造を定義できます。 +例えば、「何かを入れる箱 (Box)」のような汎用的な型を作る場合に便利です。 + +```ts:generic_box.ts +// T型の値を持つ value プロパティがあるインターフェース +interface Box { + value: T; +} + +// 文字列を入れる箱 +const stringBox: Box = { + value: "TypeScript" +}; + +// 数値を入れる箱 +const numberBox: Box = { + value: 42 +}; + +console.log(stringBox.value.toUpperCase()); // 文字列のメソッドが使える +console.log(numberBox.value.toFixed(1)); // 数値のメソッドが使える +``` + +```ts-exec:generic_box.ts +TYPESCRIPT +42.0 +``` +```js-readonly:generic_box.js +``` + +JavaScriptでは特に意識せずオブジェクトに様々な型の値を入れていましたが、TypeScriptではこのようにジェネリクスを使うことで、「中身が何かわからない」状態を防ぎつつ、どんな型でも許容する構造を作れます。 + +## Genericsクラス + +クラスでも同様にジェネリクスを使用できます。リストやキュー、スタックなどのデータ構造を実装する際によく使われます。 + +ここではシンプルな「スタック(後入れ先出し)」クラスを作ってみましょう。 + +```ts:simple_stack.ts +class SimpleStack { + private items: T[] = []; + + // データを追加する + push(item: T): void { + this.items.push(item); + } + + // データを取り出す + pop(): T | undefined { + return this.items.pop(); + } + + // 現在の中身を表示(デバッグ用) + print(): void { + console.log(this.items); + } +} + +// 数値専用のスタック +const numberStack = new SimpleStack(); +numberStack.push(10); +numberStack.push(20); +// numberStack.push("30"); // エラー: number以外は入れられない +console.log("Pop:", numberStack.pop()); + +// 文字列専用のスタック +const stringStack = new SimpleStack(); +stringStack.push("A"); +stringStack.push("B"); +stringStack.print(); +``` + +```ts-exec:simple_stack.ts +Pop: 20 +[ 'A', 'B' ] +``` +```js-readonly:simple_stack.js +``` + +もしジェネリクスを使わずにこれを実装しようとすると、`NumberStack`クラスと`StringStack`クラスを個別に作るか、`any`を使って安全性を犠牲にするしかありません。ジェネリクスを使えば、1つのクラス定義で安全に様々な型に対応できます。 + +## 型制約 (extends): Generics型に制約を設ける + +ジェネリクスは「どんな型でも受け入れられる」のが基本ですが、時には「ある特定の条件を満たす型だけを受け入れたい」という場合があります。 + +例えば、引数の `.length` プロパティにアクセスしたい場合を考えてみましょう。 + +```ts:without_constraints.ts +function logLength(arg: T): void { + console.log(arg.length); // エラー! Tがlengthを持っているとは限らない +} +``` +```ts-exec:without_constraints.ts +without_constraints.ts:2:19 - error TS2339: Property 'length' does not exist on type 'T'. + +2 console.log(arg.length); // エラー! Tがlengthを持っているとは限らない + ~~~~~~ +``` +```js-readonly:without_constraints.js +``` + +すべての型が `length` を持っているわけではない(例: `number`型にはない)ため、TypeScriptはエラーを出します。 +これを解決するために、`extends` キーワードを使って **「T は少なくともこの型を継承(適合)していなければならない」** という制約(Constraint)を設けます。 + +```ts:constraints.ts +// lengthプロパティを持つ型を定義 +interface Lengthy { + length: number; +} + +// T は Lengthy インターフェースを満たす型でなければならない +function logLength(arg: T): void { + console.log(`値: ${JSON.stringify(arg)}, 長さ: ${arg.length}`); +} + +// 配列は length を持つのでOK +logLength([1, 2, 3]); + +// 文字列も length を持つのでOK +logLength("Hello"); + +// オブジェクトも length プロパティがあればOK +logLength({ length: 10, value: "something" }); + +// 数値は length を持たないのでエラーになる +// logLength(100); +``` + +```ts-exec:constraints.ts +値: [1,2,3], 長さ: 3 +値: "Hello", 長さ: 5 +値: {"length":10,"value":"something"}, 長さ: 10 +``` +```js-readonly:constraints.js +``` + +このように `extends` を使うことで、ジェネリクスの柔軟性を保ちつつ、関数内で安全に特定のプロパティやメソッドを利用することができます。 + +## この章のまとめ + + * **ジェネリクス (Generics)** は、型を引数のように扱い、コードの再利用性と型安全性を両立させる機能です。 + * **``** のように型変数を宣言して使用します。 + * **関数、インターフェース、クラス** などで利用可能です。 + * **`extends`** キーワードを使用することで、受け入れる型に制約(「最低限このプロパティを持っていること」など)を与えることができます。 + +ジェネリクスを理解すると、ライブラリの型定義ファイル(`.d.ts`)も読みやすくなり、TypeScriptでの開発力が一気に向上します。 + +### 練習問題 1: ペアを作成する関数 + +2つの引数を受け取り、それらを配列(タプル)にして返すジェネリクス関数 `createPair` を作成してください。 +第1引数と第2引数は異なる型でも構いません。 + +**要件:** + + * 型引数を2つ(例: `T`, `U`)使用すること。 + * 戻り値の型は `[T, U]` となること。 + +```ts:practice6_1.ts +// ここに関数を定義してください +function createPair(first: T, second: U): [T, U] { + // 実装 + return [first, second]; +} + +// 実行例 +const pair1 = createPair("score", 100); +console.log(pair1); // ["score", 100] + +const pair2 = createPair(true, "valid"); +console.log(pair2); // [true, "valid"] +``` +```ts-exec:practice6_1.ts +``` +```js-readonly:practice6_1.js +``` + +### 練習問題 2: 制約付きジェネリクス + +`id` プロパティ(型は `number` または `string`)を持つオブジェクトのみを受け取り、その `id` を表示する関数 `showId` を作成してください。 + +**要件:** + + * `extends` を使用して型パラメータに制約をかけること。 + * `id` プロパティを持たないオブジェクトを渡すとコンパイルエラーになること。 + +```ts:practice6_2.ts +// 制約用のインターフェース +interface HasId { + id: number | string; +} + +// ここに関数を定義してください +function showId(item: T): void { + console.log(`ID is: ${item.id}`); +} + +// 実行例 +showId({ id: 1, name: "UserA" }); // OK +showId({ id: "abc-123", active: true }); // OK + +// 以下のコードはエラーになるはずです +// showId({ name: "NoIdUser" }); +``` +```ts-exec:practice6_2.ts +``` +```js-readonly:practice6_2.js +``` diff --git a/public/docs/typescript-7.md b/public/docs/typescript-7.md new file mode 100644 index 0000000..20be426 --- /dev/null +++ b/public/docs/typescript-7.md @@ -0,0 +1,306 @@ +# 第7章: クラスとアクセス修飾子 + +JavaScript(ES6以降)に慣れ親しんでいる方であれば、`class`構文自体はすでにご存知かと思います。TypeScriptにおけるクラスは、JavaScriptのクラス機能をベースにしつつ、**型安全性**と**アクセス制御(カプセル化)**を強化するための機能が追加されています。 + +本章では、TypeScript特有のクラスの書き方、特にプロパティの定義、アクセス修飾子、そしてインターフェースとの連携について学びます。 + +## JSのクラス構文の復習: constructor, extends + +まずは、基本的なJavaScriptのクラス構文をTypeScriptのファイルとして書いてみましょう。TypeScriptはJavaScriptのスーパーセット(上位互換)であるため、標準的なJSの書き方もほぼそのまま動作しますが、少しだけ「型」の意識が必要です。 + +```ts:basic-animal.ts +class Animal { + // TypeScriptでは、ここでプロパティ(フィールド)を宣言するのが一般的ですが、 + // JSのようにconstructor内でthis.name = nameするだけだとエラーになることがあります。 + // (詳しくは次のセクションで解説します) + name: string; + + constructor(name: string) { + this.name = name; + } + + move(distanceInMeters: number = 0) { + console.log(`${this.name} moved ${distanceInMeters}m.`); + } +} + +class Snake extends Animal { + constructor(name: string) { + // 派生クラスのコンストラクタでは super() の呼び出しが必須 + super(name); + } + + move(distanceInMeters: number = 5) { + console.log("Slithering..."); + super.move(distanceInMeters); + } +} + +const sam = new Snake("Sammy the Python"); +sam.move(); +``` + +```ts-exec:basic-animal.ts +Slithering... +Sammy the Python moved 5m. +``` +```js-readonly:basic-animal.js +``` + +基本構造はJSと同じですが、引数に型注釈(`: string`, `: number`)が付いている点が異なります。 + +## TypeScriptのクラス: プロパティの型定義 + +JavaScriptでは、コンストラクタ内で `this.x = 10` と書くだけでプロパティを追加できましたが、TypeScriptでは**クラスの直下(ボディ)でプロパティとその型を宣言する**必要があります。 + +これを省略すると、「プロパティ 'x' は型 'ClassName' に存在しません」というエラーになります。 + +```ts:property-definition.ts +class Product { + // プロパティの宣言(必須) + id: number; + name: string; + price: number; + + constructor(id: number, name: string, price: number) { + this.id = id; + this.name = name; + this.price = price; + } + + getDetail(): string { + return `ID:${this.id} ${this.name} (${this.price}円)`; + } +} + +const item = new Product(1, "TypeScript入門書", 2500); +console.log(item.getDetail()); +``` + +```ts-exec:property-definition.ts +ID:1 TypeScript入門書 (2500円) +``` +```js-readonly:property-definition.js +``` + +> **注意:** `strictPropertyInitialization` 設定(tsconfig.json)が有効な場合、プロパティを宣言したもののコンストラクタで初期化していないとエラーになります。初期化を後で行うことが確実な場合は `name!: string;` のように `!` を付けて警告を抑制することもあります。 + +## アクセス修飾子: public, private, protected + +TypeScriptには、クラスのメンバー(プロパティやメソッド)へのアクセスを制御するための3つの修飾子があります。これはJavaやC\#などの言語と同様の概念です。 + +1. **`public` (デフォルト)**: どこからでもアクセス可能。 +2. **`private`**: 定義されたクラスの内部からのみアクセス可能。 +3. **`protected`**: 定義されたクラス、およびそのサブクラス(継承先)からアクセス可能。 + +### 従来の書き方と省略記法(パラメータプロパティ) + +TypeScriptには、コンストラクタの引数にアクセス修飾子を付けることで、**「プロパティ宣言」と「代入」を同時に行う省略記法(パラメータプロパティ)**があります。実務ではこの書き方が非常によく使われます。 + +```ts:access-modifiers.ts +class User { + // 通常の書き方 + public name: string; + private _age: number; // 慣習的にprivateフィールドには_をつけることがあります + + // 省略記法(パラメータプロパティ) + // constructor引数に修飾子をつけることで、自動的にプロパティとして定義・代入される + constructor(name: string, age: number, protected email: string) { + this.name = name; + this._age = age; + // this.email = email; // 自動で行われるため記述不要 + } + + public getProfile(): string { + // privateやprotectedはクラス内部ではアクセス可能 + return `${this.name} (${this._age}) - ${this.email}`; + } +} + +const user = new User("Alice", 30, "alice@example.com"); + +console.log(user.name); // OK (public) +console.log(user.getProfile()); // OK (public) + +// 以下の行はコンパイルエラーになります +// console.log(user._age); // Error: Property '_age' is private... +// console.log(user.email); // Error: Property 'email' is protected... +``` + +```ts-exec:access-modifiers.ts +Alice +Alice (30) - alice@example.com +``` + +```js-readonly:access-modifiers.js +``` + +> **Note:** TypeScriptの `private` はあくまでコンパイル時のチェックです。JavaScriptにトランスパイルされると単なるプロパティになるため、実行時にはアクセスしようと思えばできてしまいます。厳密な実行時プライベートが必要な場合は、JavaScript標準の `#` (例: `#field`) を使用してください。 + +## readonly修飾子: クラスプロパティへの適用 + +`readonly` 修飾子を付けると、そのプロパティは**読み取り専用**になります。 +値の代入は「プロパティ宣言時」または「コンストラクタ内」でのみ許可されます。 + +```ts:readonly-modifier.ts +class Configuration { + // 宣言時に初期化 + readonly version: string = "1.0.0"; + readonly apiKey: string; + + constructor(apiKey: string) { + // コンストラクタ内での代入はOK + this.apiKey = apiKey; + } + + updateConfig() { + // エラー: 読み取り専用プロパティに代入しようとしています + // this.version = "2.0.0"; + } +} + +const config = new Configuration("xyz-123"); +console.log(`Version: ${config.version}, Key: ${config.apiKey}`); + +// エラー: クラスの外からも変更不可 +// config.apiKey = "abc-999"; +``` + +```ts-exec:readonly-modifier.ts +Version: 1.0.0, Key: xyz-123 +``` +```js-readonly:readonly-modifier.js +``` + +## implements: インターフェースによるクラスの形状の強制 + +第3章で学んだインターフェースは、オブジェクトの型定義だけでなく、**クラスが特定の実装を持っていることを保証する(契約を結ぶ)**ためにも使われます。これを `implements` と呼びます。 + +```ts:implements-interface.ts +interface Printable { + print(): void; +} + +interface Loggable { + log(message: string): void; +} + +// 複数のインターフェースを実装可能 +class DocumentFile implements Printable, Loggable { + constructor(private title: string) {} + + // Printableの実装 + print() { + console.log(`Printing document: ${this.title}...`); + } + + // Loggableの実装 + log(message: string) { + console.log(`[LOG]: ${message}`); + } +} + +const doc = new DocumentFile("ProjectPlan.pdf"); +doc.print(); +doc.log("Print job started"); +``` + +```ts-exec:implements-interface.ts +Printing document: ProjectPlan.pdf... +[LOG]: Print job started +``` +```js-readonly:implements-interface.js +``` + +もし `print()` メソッドを実装し忘れると、TypeScriptコンパイラは即座にエラーを出します。これにより、大規模開発での実装漏れを防げます。 + +## 抽象クラス (abstract): 継承専用の基底クラス + +「インスタンス化はさせたくないが、共通の機能を継承させたい」場合や、「メソッドの名前だけ決めておいて、具体的な処理はサブクラスに任せたい」場合に **抽象クラス (`abstract class`)** を使用します。 + + * `abstract` クラス: `new` で直接インスタンス化できません。 + * `abstract` メソッド: 実装(中身)を持ちません。継承先のクラスで必ず実装する必要があります。 + +```ts:abstract-class.ts +abstract class Shape { + constructor(protected color: string) {} + + // 具体的な実装を持つメソッド + describe(): void { + console.log(`This is a ${this.color} shape.`); + } + + // 抽象メソッド(署名のみ定義) + // サブクラスで必ず getArea を実装しなければならない + abstract getArea(): number; +} + +class Circle extends Shape { + constructor(color: string, private radius: number) { + super(color); + } + + // 抽象メソッドの実装 + getArea(): number { + return Math.PI * this.radius ** 2; + } +} + +// const shape = new Shape("red"); // エラー: 抽象クラスはインスタンス化できない + +const circle = new Circle("blue", 5); +circle.describe(); // 親クラスのメソッド +console.log(`Area: ${circle.getArea().toFixed(2)}`); // 実装したメソッド +``` + +```ts-exec:abstract-class.ts +This is a blue shape. +Area: 78.54 +``` +```js-readonly:abstract-class.js +``` + +## この章のまとめ + + * **プロパティ定義:** TypeScriptではクラスボディ内でプロパティの宣言が必要です。 + * **アクセス修飾子:** `public`, `private`, `protected` でカプセル化を制御します。 + * **パラメータプロパティ:** コンストラクタ引数に修飾子をつけることで、宣言と初期化を簡潔に書けます。 + * **readonly:** プロパティを不変(読み取り専用)にします。 + * **implements:** クラスが特定のインターフェースの仕様を満たすことを強制します。 + * **abstract:** インスタンス化できない基底クラスや、実装を強制する抽象メソッドを定義します。 + +### 練習問題 1: 従業員クラスの作成 + +以下の要件を満たす `Employee` クラスを作成し、動作確認コードを書いてください。 + +1. **プロパティ**: + * `name` (string): パブリック + * `id` (number): 読み取り専用 + * `salary` (number): プライベート +2. **コンストラクタ**: 省略記法(パラメータプロパティ)を使ってこれらを初期化してください。 +3. **メソッド**: + * `getSalaryInfo()`: "従業員 [name] の給与は [salary] です" と出力するメソッド(クラス内部からは `salary` にアクセスできることを確認)。 + +```ts:practice7_1.ts +``` +```ts-exec:practice7_1.ts +``` +```js-readonly:practice7_1.js +``` + +### 練習問題 2: 図形クラスの継承 + +以下の要件でコードを書いてください。 + +1. **インターフェース `AreaCalculator`**: `calculateArea(): number` メソッドを持つ。 +2. **クラス `Rectangle`**: `AreaCalculator` を実装(`implements`)する。 + * プロパティ: `width` (number), `height` (number) + * メソッド: `calculateArea` を実装して面積を返す。 +3. `Rectangle` のインスタンスを作成し、面積をコンソールに出力してください。 + +```ts:practice7_2.ts +``` +```ts-exec:practice7_2.ts +``` +```js-readonly:practice7_2.js +``` \ No newline at end of file diff --git a/public/docs/typescript-8.md b/public/docs/typescript-8.md new file mode 100644 index 0000000..8a39192 --- /dev/null +++ b/public/docs/typescript-8.md @@ -0,0 +1,285 @@ +# 第8章: 非同期処理とユーティリティ型 + +JavaScriptにおいて `Promise` や `async/await` は日常的に使用しますが、TypeScriptでは「将来どのような値が返ってくるか」を明示する必要があります。 +また、既存の型を再利用して新しい型を作る「ユーティリティ型」を学ぶことで、コードの重複を劇的に減らすことができます。 + +## 非同期処理の型: Promise と async/await + +JavaScriptでは、非同期関数の戻り値は常に `Promise` オブジェクトです。TypeScriptでは、このPromiseが**解決(Resolve)されたときに持つ値の型**をジェネリクスを使って `Promise` の形式で表現します。 + +### 基本的な定義 + + * 戻り値が文字列の場合: `Promise` + * 戻り値が数値の場合: `Promise` + * 戻り値がない(void)場合: `Promise` + +`async` キーワードがついた関数は、自動的に戻り値が `Promise` でラップされます。 + +```ts:async-fetch.ts +type User = { + id: number; + name: string; + email: string; +}; + +// 擬似的なAPIコール関数 +// 戻り値の型として Promise を指定します +const fetchUser = async (userId: number): Promise => { + // 実際はfetchなどを行いますが、ここでは擬似的に遅延させて値を返します + return new Promise((resolve) => { + setTimeout(() => { + resolve({ + id: userId, + name: "Yamada Taro", + email: "taro@example.com", + }); + }, 500); + }); +}; + +const main = async () => { + console.log("Fetching data..."); + + // awaitを使うことで、user変数の型は自動的に User 型(Promiseが解けた状態)になります + const user = await fetchUser(1); + + console.log(`ID: ${user.id}`); + console.log(`Name: ${user.name}`); +}; + +main(); +``` + +```ts-exec:async-fetch.ts +Fetching data... +ID: 1 +Name: Yamada Taro +``` +```js-readonly:async-fetch.js +``` + +### エラーハンドリングと型 + +Promiseが拒否(Reject)される場合のエラー型は、現状のTypeScriptではデフォルトで `any` または `unknown` として扱われます(`try-catch` ブロックの `error` オブジェクトなど)。 + +## ユーティリティ型 (Utility Types) + +TypeScriptには、既存の型定義を変換して新しい型を生成するための便利な型が標準で用意されています。これらを使うと、「一部のプロパティだけ変更したい」「全てオプショナルにしたい」といった場合に、いちいち新しい型を定義し直す必要がなくなります。 + +ここでは、特によく使われる4つのユーティリティ型を紹介します。 + +### ベースとなる型 + +以下の `Product` 型を例に使用します。 + +```ts +interface Product { + id: number; + name: string; + price: number; + description: string; +} +``` + +### 1\. Partial\: 全てをオプショナルにする + +`Partial` は、型 `T` のすべてのプロパティを「必須」から「任意(Optional / `?`付き)」に変更します。データの更新処理(パッチ)などで、一部のフィールドだけ送信したい場合に便利です。 + +```ts:utility-partial.ts +interface Product { + id: number; + name: string; + price: number; + description: string; +} + +// プロパティの一部だけを更新する関数 +// updateDataは { name?: string; price?: number; ... } のようになります +function updateProduct(id: number, updateData: Partial) { + console.log(`Updating product ${id} with:`, updateData); +} + +// nameとpriceだけ更新(descriptionやidがなくてもエラーにならない) +updateProduct(100, { + name: "New Product Name", + price: 5000 +}); +``` + +```ts-exec:utility-partial.ts +Updating product 100 with: { name: 'New Product Name', price: 5000 } +``` +```js-readonly:utility-partial.js +``` + +### 2\. Readonly\: 全てを読み取り専用にする + +`Readonly` は、型 `T` のすべてのプロパティを書き換え不可(readonly)にします。関数内でオブジェクトを変更されたくない場合や、ReactのState管理などで役立ちます。 + +```ts:utility-readonly.ts +interface Product { + id: number; + name: string; + price: number; +} + +const originalProduct: Product = { id: 1, name: "Pen", price: 100 }; + +// 変更不可のオブジェクトとして扱う +const frozenProduct: Readonly = originalProduct; + +// 読み取りはOK +console.log(frozenProduct.name); + +// コンパイルエラー: 値の代入はできません +// frozenProduct.price = 200; +``` + +```ts-exec:utility-readonly.ts +Pen +``` +```js-readonly:utility-readonly.js +``` + +### 3\. Pick\: 特定のキーだけ抜き出す + +`Pick` は、型 `T` から `K` で指定したプロパティのみを抽出して新しい型を作ります。 +「ユーザー情報全体から、表示用の名前と画像URLだけ欲しい」といった場合に使います。 + +```ts:utility-pick.ts +interface Product { + id: number; + name: string; + price: number; + description: string; + stock: number; +} + +// 商品一覧表示用に、IDと名前と価格だけが必要な型を作る +type ProductPreview = Pick; + +const item: ProductPreview = { + id: 1, + name: "Laptop", + price: 120000, + // description: "..." // エラー: ProductPreviewにはdescriptionは存在しません +}; + +console.log(item); +``` + +```ts-exec:utility-pick.ts +{ id: 1, name: 'Laptop', price: 120000 } +``` +```js-readonly:utility-pick.js +``` + +### 4\. Omit\: 特定のキーだけ除外する + +`Omit` は `Pick` の逆で、指定したプロパティを除外します。 +「データベースのモデルから、機密情報や内部管理用のIDを除外してクライアントに返したい」といった場合に有用です。 + +```ts:utility-omit.ts +interface Product { + id: number; + name: string; + price: number; + secretCode: string; // 外部に出したくない情報 + internalId: string; // 外部に出したくない情報 +} + +// 外部公開用の型(secretCodeとinternalIdを除外) +type PublicProduct = Omit; + +const publicItem: PublicProduct = { + id: 1, + name: "Mouse", + price: 3000 +}; + +console.log(publicItem); +``` + +```ts-exec:utility-omit.ts +{ id: 1, name: 'Mouse', price: 3000 } +``` +```js-readonly:utility-omit.js +``` + +## 高度な型操作(概要) + +ここでは詳細な文法までは踏み込みませんが、ライブラリの型定義などを読む際に遭遇する高度な概念を紹介します。これらは上記のユーティリティ型の内部実装にも使われています。 + +### Mapped Types (マップ型) + +既存の型のプロパティをループ処理して、新しい型を作る機能です。配列の `.map()` の型バージョンと考えると分かりやすいでしょう。 + +```ts +type Item = { a: string; b: number }; + +// 既存のItemのキー(P)をすべて boolean 型に変換する +type BooleanItem = { + [P in keyof Item]: boolean; +}; +// 結果: { a: boolean; b: boolean; } と等価 +``` + +### Conditional Types (条件付き型) + +型の三項演算子のようなものです。「もし型Tが型Uを継承しているならX型、そうでなければY型」という条件分岐を定義できます。 + +```ts +// Tがstringなら number[] を、それ以外なら T[] を返す型 +type StringArrayOrGeneric = T extends string ? number[] : T[]; + +type A = StringArrayOrGeneric; // number[] になる +type B = StringArrayOrGeneric; // boolean[] になる +``` + +## この章のまとめ + + * **非同期処理**: `async` 関数の戻り値は `Promise` で定義する。 + * **Utility Types**: TypeScriptには型の再利用性を高める便利な型が組み込まれている。 + * `Partial`: 全プロパティを任意にする。 + * `Readonly`: 全プロパティを読み取り専用にする。 + * `Pick`: 必要なプロパティだけ抽出する。 + * `Omit`: 不要なプロパティを除外する。 + * **高度な型**: `Mapped Types` や `Conditional Types` を使うことで、動的で柔軟な型定義が可能になる。 + +JavaScriptの柔軟性を保ちつつ、堅牢さを加えるためにこれらの機能は非常に重要です。特にユーティリティ型は、冗長なコードを減らす即戦力の機能ですので、ぜひ活用してください。 + +### 練習問題1: 非同期データの取得 + +1. `Post` というインターフェースを定義してください(`id: number`, `title: string`, `body: string`)。 +2. `fetchPost` という `async` 関数を作成してください。この関数は引数に `id` (number) を受け取り、戻り値として `Promise` を返します。 +3. 関数内部では、引数で受け取ったデータをそのまま含むオブジェクトを返してください(`setTimeout`などは不要です)。 +4. 作成した関数を実行し、結果をコンソールに表示してください。 + +```ts:practice8_1.ts +``` +```ts-exec:practice8_1.ts +``` +```js-readonly:practice8_1.js +``` + +### 練習問題2: ユーティリティ型の活用 + +アプリケーションの設定を表す `AppConfig` インターフェースがあります。 +以下の要件を満たす新しい型と変数を定義してください。 + +1. `AppConfig` から `debugMode` を**除外**した型 `ProductionConfig` を定義してください (`Omit`を使用)。 +2. `AppConfig` のすべてのプロパティを**任意(Optional)**にした型 `OptionalConfig` を定義してください (`Partial`を使用)。 +3. `ProductionConfig` 型を持つ変数 `prodConfig` を定義し、適切な値を代入してください。 + +```ts:practice8_2.ts +interface AppConfig { + apiUrl: string; + retryCount: number; + timeout: number; + debugMode: boolean; +} +``` +```ts-exec:practice8_2.ts +``` +```js-readonly:practice8_2.js +```