# TypeScript 基礎應用入門,並與 Next.js 進行整合開發 ## 第 1 章:TypeScript 簡介與環境設置 **TypeScript = JS 超集 + 靜態型別系統**。藉由編譯期偵錯、完善的 IDE 體驗(自動完成、跳轉、重構),提升大型專案的可靠性與維護性。 ### 推薦安裝方式(本地開發依賴) > 不建議全域安裝 `tsc` 當作日常開發主流程,改用專案本地依賴更易控版本。 ```bash # 初始化專案(空資料夾) npm init -y # 安裝 TS 與型別(開發依賴) npm i -D typescript @types/node # 產生 tsconfig.json npx tsc --init ``` ### tsconfig 建議(現代前端) ```jsonc { "compilerOptions": { "target": "ES2020", "lib": ["ES2020", "DOM", "DOM.Iterable"], "module": "ESNext", "moduleResolution": "Node", "jsx": "react-jsx", "strict": true, "noFallthroughCasesInSwitch": true, "esModuleInterop": true, "skipLibCheck": true, "forceConsistentCasingInFileNames": true, "resolveJsonModule": true, "isolatedModules": true }, "include": ["src"] } ``` > 若需 IE11(極少數舊專案)才考慮 `target: "ES5"`;現代 React 不再支援 IE11。 ### Hello, TypeScript ```ts // src/hello.ts console.log("Hello, TypeScript!") ``` ```bash npx tsc src/hello.ts && node src/hello.js ``` 參考:`feature/typescript`、`feature/typescript-hello`(你的範例倉庫)。 --- ## 第 2 章:基本型別(精要) * **number / string / boolean**:`let n: number = 42`、模板字面量型別可組裝字串。 * **null / undefined**:常見於可空欄位,與 `strictNullChecks` 搭配更安全。 * **any vs unknown**:`unknown` 更安全(需先縮小/斷言才能使用)。 * **void / never**:`void` 表示不回傳;`never` 表示永不返回(throw 或無限迴圈)。 * **object**:非原始型別;在專案中更常使用自定義物件型別。 * **symbol**:唯一鍵;偶見於進階框架開發。 * **型別推斷**:TS 會從初始值推斷型別,善用推斷可減少噪音。 --- ## 第 3 章:變數、常量與作用域 ```ts let age: number = 25 const name = "John" // const 推斷為字面量型別,可更精準 // name = "Doe" // ❌ 不能再賦值 ``` > 與 `var` 相比,`let/const` 具塊級作用域,避免提升與重複宣告問題。 --- ## 第 4 章:函式與箭頭函式 ```ts function greet(name: string = "Anonymous"): string { return `Hello, ${name}!` } // 箭頭函式 + 明確回傳型別 const add = (a: number, b: number): number => a + b // 可選參數 function log(msg: string, level?: "info" | "warn" | "error") { console[level ?? "info"]?.(msg) } // 函式多載(overload) function toArray(x: string): string[] function toArray(x: number): number[] function toArray(x: string | number) { return [x] } ``` --- ## 第 5 章:物件、陣列、元組 ```ts // 物件 const person: { name: string; age?: number; readonly id: string } = { id: "u_1", name: "Alice" } // 陣列 const nums: number[] = [1,2,3] const words: Array<string> = ["a","b"] // 等價寫法 // 只讀陣列 const readonlyNums: readonly number[] = [1,2,3] // readonlyNums.push(4) // ❌ // 元組 const userEntry: [id: string, score: number] = ["u_1", 98] // 聯合型別陣列 const mixed: (number | string)[] = [1, "two", 3] ``` --- ## 第 6 章:`type` 與 `interface` **相同點**:都可描述物件形狀、支援可選屬性與函式型別。 **差異與建議**: * `interface` 支援多次宣告合併、`extends`(在 SDK/公共 API 常用)。 * `type` 支援 **聯合 / 交叉 / 條件 / 映射** 型別,組合力更強。 * 在 React 專案中: * **Props/State** 可用 `type` 或 `interface` 皆可;偏好多用 `type` 來搭配聯合與工具型別。 ```ts type User = { id: string; name: string } interface Node { id: string } interface Person extends Node { name: string } type Employee = User & { role: "dev" | "pm" } ``` --- ## 第 7 章:Class(瞭解即可) React 函式元件為主流,Class 在前端較少見;但在工具程式或 OOP 情境仍實用。 ```ts class Dog { breed = "Mixed" name = "Unknown" bark() { console.log("Woof!") } } const d = new Dog(); d.name = "Buddy"; d.bark() ``` --- ## 第 8 章:Enum 與替代方案 ```ts enum Direction { Up = "UP", Down = "DOWN", Left = "LEFT", Right = "RIGHT" } ``` > 前端專案更常用 **字串聯合型別** 取代 `enum`,因為 tree-shaking 友善、可讀值直接就是字串: ```ts type Direction2 = "UP" | "DOWN" | "LEFT" | "RIGHT" ``` > `const enum` 會 inline 值、體積小,但與 `isolatedModules`/Babel 流程相處不佳,慎用。 --- ## 第 9 章:泛型(Generics) ```ts function identity<T>(x: T): T { return x } const a = identity<string>("hi") const b = identity(42) // 推斷為 number class Box<T> { constructor(public value: T) {} } const s = new Box<string>("txt") interface Pair<K, V> { key: K; value: V } const p: Pair<string, number> = { key: "age", value: 30 } // 受限泛型 interface HasLength { length: number } function logLength<T extends HasLength>(item: T) { console.log(item.length) } ``` --- # 第二部分:在 React 中使用 TypeScript ## 建立 React + TS 專案 **Vite(推薦)** ```bash npm create vite@latest my-app -- --template react-ts cd my-app && npm i && npm run dev ``` **Create React App(舊方案)** ```bash npx create-react-app my-app --template typescript ``` ## tsconfig(React 專案範例) ```jsonc { "compilerOptions": { "target": "ES2020", "lib": ["ES2020", "DOM", "DOM.Iterable"], "module": "ESNext", "moduleResolution": "Node", "jsx": "react-jsx", "strict": true, "esModuleInterop": true, "skipLibCheck": true, "resolveJsonModule": true, "isolatedModules": true, "baseUrl": ".", "paths": { "@/*": ["src/*"] } }, "include": ["src"] } ``` **Vite 路徑別名**(`vite.config.ts`) ```ts import { defineConfig } from 'vite' import react from '@vitejs/plugin-react' import path from 'path' export default defineConfig({ plugins: [react()], resolve: { alias: { '@': path.resolve(__dirname, 'src') } } }) ``` ## 元件與 Props 型別 ```tsx // 方式一:具名 Props 型別 + 傳統函式 type GreetingProps = { name: string; children?: React.ReactNode } function Greeting({ name, children }: GreetingProps) { return <h1>Hello, {name}! {children}</h1> } // 方式二:React.FC(注意 children 已內建,但不易自訂 defaultProps) interface ButtonProps { onClick?: () => void; disabled?: boolean } const Button: React.FC<ButtonProps> = ({ children, ...rest }) => ( <button {...rest}>{children}</button> ) ``` > 實務上 **不一定要用 `React.FC`**;直接為函式標註 props 型別即可,較直觀。 ## 事件與 DOM 型別 ```tsx function SearchBox() { const [q, setQ] = React.useState("") const onChange = (e: React.ChangeEvent<HTMLInputElement>) => setQ(e.target.value) const onSubmit = (e: React.FormEvent<HTMLFormElement>) => { e.preventDefault() } return ( <form onSubmit={onSubmit}> <input value={q} onChange={onChange} /> <button>Go</button> </form> ) } ``` ## 狀態與可空型別 ```tsx const [user, setUser] = React.useState<null | { id: string; name: string }>(null) // 之後以 type narrowing 使用 user?.name 或條件判斷 ``` ## Context 與自訂 Hook 型別 ```tsx // Context type Auth = { user: { id: string; name: string } | null; login: () => void } const AuthCtx = React.createContext<Auth | undefined>(undefined) function useAuth() { const ctx = React.useContext(AuthCtx) if (!ctx) throw new Error('useAuth must be used within <AuthProvider>') return ctx } ``` ## CSS Modules 型別宣告 ```ts // src/global.d.ts declare module '*.module.css' { const classes: Record<string, string>; export default classes } declare module '*.module.scss' { const classes: Record<string, string>; export default classes } ``` ## 常見第三方型別 ```bash npm i -D @types/react-router-dom @types/lodash ``` > 很多套件已內建型別,否則在 DefinitelyTyped 以 `@types/` 取得。 --- # 第三部分:在現有 React 專案中引入 TypeScript(增量遷移) ## 步驟總覽 1. 安裝依賴: ```bash npm i -D typescript @types/node @types/react @types/react-dom ``` 2. 新增 `tsconfig.json`(見上方建議)。 3. **逐檔改名**:`.js`→`.ts`、`.jsx`→`.tsx`。 4. **允許混用**:暫時保留 `.js`(`allowJs: true`)。也可用 `// @ts-check` + JSDoc 在 JS 中先享型別檢查。 5. 逐步加入型別註解(props、state、事件、API 回傳)。 6. `npx tsc --noEmit` 持續檢查,修正錯誤。 參考:`feature/app`、`feature/react-typescript-init`(你的範例倉庫)。 ## 例:JS → TS 改寫 ```tsx // App.tsx import { useState } from 'react' export default function App() { const [count, setCount] = useState<number>(0) return ( <div> <p>You clicked {count} times</p> <button onClick={() => setCount(count + 1)}>Click me</button> </div> ) } ``` ## API 回傳型別與資料安全 ```ts type Todo = { id: number; title: string; completed: boolean } async function fetchTodos(): Promise<Todo[]> { const r = await fetch('/api/todos') const data = await r.json() as Todo[] // 更安全:配合 zod/io-ts 驗證 return data } ``` --- # 第四部分:工具化(ESLint / Prettier / tsconfig 嚴格度) ## ESLint + typescript-eslint(Vite 預設可擴充) ```bash npm i -D eslint @typescript-eslint/parser @typescript-eslint/eslint-plugin ``` ```js // .eslintrc.cjs module.exports = { parser: '@typescript-eslint/parser', plugins: ['@typescript-eslint', 'react', 'react-hooks'], extends: [ 'eslint:recommended', 'plugin:@typescript-eslint/recommended', 'plugin:react/recommended', 'plugin:react-hooks/recommended' ], settings: { react: { version: 'detect' } } } ``` ## Prettier(格式化) ```bash npm i -D prettier eslint-config-prettier eslint-plugin-prettier ``` ```js // .prettierrc.json { "singleQuote": true, "semi": false, "trailingComma": "es5" } ``` ## 嚴格度微調(tsconfig) * `strict: true`(建議) * 常用:`noImplicitAny`, `strictNullChecks`, `noUncheckedIndexedAccess`(更嚴謹) --- # 第五部分:常見踩坑與最佳實務 * **`any` 氾濫** → 以 `unknown` / 型別守衛 / 泛型替代。 * **`React.FC` 一律使用?** → 不必。直接為函式標 props 型別更靈活。 * **`defaultProps` in FC** → 已不推薦;用參數預設值或 `Partial<T>`。 * **可空狀態** → 初始 `null` 會變 `T | null`,渲染前加守衛或 `?.`。 * **事件型別** → `React.ChangeEvent<HTMLInputElement>`、`React.MouseEvent<HTMLButtonElement>`。 * **ref / forwardRef** → `const Comp = React.forwardRef<HTMLDivElement, Props>((props, ref) => ...)`。 * **CSS Modules 型別** → 記得加 `global.d.ts` 宣告。 * **路徑別名** → tsconfig + bundler(Vite/webpack)需一致。 * **Enum vs 字串聯合** → 前端多用字串聯合;必要時才上 enum。 * **工具型別** → `Partial`/`Pick`/`Omit`/`Record`/`ReturnType`/`Parameters`/`NonNullable` 善用。 * **`satisfies`**(TS 4.9+) → 保留推斷又驗證結構: ```ts const routes = { home: { path: '/' }, about: { path: '/about' } } as const satisfies Record<string, { path: string }> ``` --- # 第六部分:小型實作練習清單 1. **型別化 TodoList**:`Todo` 型別、API 型別、`useReducer` 管理狀態。 2. **表單元件庫**:輸入元件 `value/onChange` 泛型化,事件型別正確。 3. **資料抓取 hooks**:`useFetch<T>()` 泛型回傳,搭配 `AbortController`。 4. **路由型別**:`useParams<{ id: string }>()`(React Router v6)。 5. **Context + Reducer**:以型別守衛確保 `dispatch` 的 action payload 正確。 --- ## 參考與你的範例倉庫 * 初始化專案:`feature/typescript`、`feature/typescript-hello` * React TS 起步與遷移:`feature/app`、`feature/react-typescript-init` > 完成本教材後,你應能:讀寫 TS 型別、在 React 中自信地型別化所有常見模式、把既有 JS 專案 **增量遷移** 至 TypeScript,並建立一套可靠的團隊規範。