TypeScript 基礎
(前面的內容保持不變,此處省略)
實用型別
// TypeScript 內建實用型別
// 1. Partial<T>(所有屬性變為選用)
interface User {
id: number;
name: string;
email: string;
}
function updateUser(id: number, updates: Partial<User>): void {
// updates 可以只有部分屬性
}
updateUser(1, { name: "金哲洙" }); // OK
updateUser(2, { email: "new@example.com" }); // OK
updateUser(3, { name: "李英姬", email: "lee@example.com" }); // OK
// 2. Required<T>(所有屬性變為必填)
interface PartialUser {
id?: number;
name?: string;
email?: string;
}
type RequiredUser = Required<PartialUser>;
const user: RequiredUser = {
id: 1,
name: "金哲洙",
email: "kim@example.com"
// 所有屬性都是必填!
};
// 3. Readonly<T>(所有屬性變為唯讀)
interface Config {
apiUrl: string;
timeout: number;
}
const config: Readonly<Config> = {
apiUrl: "https://api.example.com",
timeout: 5000
};
// ❌ 無法修改
// config.apiUrl = "https://api2.example.com";
// 4. Pick<T, K>(選擇特定屬性)
interface Product {
id: number;
name: string;
price: number;
description: string;
stock: number;
}
type ProductPreview = Pick<Product, "id" | "name" | "price">;
const preview: ProductPreview = {
id: 1,
name: "筆記型電腦",
price: 1000000
// description、stock 不需要
};
// 5. Omit<T, K>(排除特定屬性)
type ProductWithoutStock = Omit<Product, "stock">;
const product: ProductWithoutStock = {
id: 1,
name: "筆記型電腦",
price: 1000000,
description: "高性能筆記型電腦"
// stock 已排除
};
// 6. Record<K, T>(鍵值對映)
type PageInfo = Record<string, { title: string; url: string }>;
const pages: PageInfo = {
home: { title: "首頁", url: "/" },
about: { title: "關於", url: "/about" },
contact: { title: "聯絡", url: "/contact" }
};
// 7. Exclude<T, U>(從聯集型別中排除型別)
type AllStatus = "pending" | "approved" | "rejected" | "draft";
type ActiveStatus = Exclude<AllStatus, "draft">;
// "pending" | "approved" | "rejected"
// 8. Extract<T, U>(從聯集型別中提取型別)
type Status = "pending" | "approved" | "rejected";
type PositiveStatus = Extract<Status, "approved">;
// "approved"
// 9. NonNullable<T>(移除 null、undefined)
type MaybeString = string | null | undefined;
type DefiniteString = NonNullable<MaybeString>;
// string
// 10. ReturnType<T>(提取函式回傳型別)
function getUser() {
return {
id: 1,
name: "金哲洙",
email: "kim@example.com"
};
}
type User = ReturnType<typeof getUser>;
// { id: number; name: string; email: string }
JavaScript → TypeScript 轉換
// JavaScript 程式碼
// user.js
function createUser(name, age, email) {
return {
name: name,
age: age,
email: email,
greet: function() {
return "Hello, " + this.name + "!";
}
};
}
function getUsers() {
return fetch('/api/users')
.then(res => res.json())
.then(data => data.users);
}
const user = createUser("金哲洙", 25, "kim@example.com");
console.log(user.greet());
// ↓ 轉換為 TypeScript
// user.ts
interface User {
name: string;
age: number;
email: string;
greet(): string;
}
function createUser(name: string, age: number, email: string): User {
return {
name,
age,
email,
greet() {
return `Hello, ${this.name}!`;
}
};
}
interface ApiResponse {
users: User[];
}
async function getUsers(): Promise<User[]> {
const response = await fetch('/api/users');
const data: ApiResponse = await response.json();
return data.users;
}
const user: User = createUser("金哲洙", 25, "kim@example.com");
console.log(user.greet());
// 優點:
// 1. 型別安全性
// 2. IDE 自動完成
// 3. 重構容易
// 4. 預防錯誤
React 中的 TypeScript
// React 元件(JavaScript)
// Button.jsx
function Button({ label, onClick, disabled }) {
return (
<button onClick={onClick} disabled={disabled}>
{label}
</button>
);
}
// ↓ 轉換為 TypeScript
// Button.tsx
interface ButtonProps {
label: string;
onClick: () => void;
disabled?: boolean;
}
function Button({ label, onClick, disabled = false }: ButtonProps) {
return (
<button onClick={onClick} disabled={disabled}>
{label}
</button>
);
}
// 或使用 React.FC
const Button: React.FC<ButtonProps> = ({ label, onClick, disabled = false }) => {
return (
<button onClick={onClick} disabled={disabled}>
{label}
</button>
);
};
// 使用
<Button
label="點擊"
onClick={() => console.log('已點擊')}
disabled={false}
/>
// ❌ 錯誤用法
<Button
label={123} // ❌ Error:label 必須是字串
onClick="handleClick" // ❌ Error:onClick 必須是函式
/>
// 2. State 和 Hooks
import { useState, useEffect } from 'react';
interface User {
id: number;
name: string;
email: string;
}
function UserProfile() {
// 指定 State 型別
const [user, setUser] = useState<User | null>(null);
const [loading, setLoading] = useState<boolean>(false);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
setLoading(true);
fetch('/api/user')
.then(res => res.json())
.then((data: User) => {
setUser(data);
setLoading(false);
})
.catch((err: Error) => {
setError(err.message);
setLoading(false);
});
}, []);
if (loading) return <div>載入中...</div>;
if (error) return <div>錯誤:{error}</div>;
if (!user) return <div>無使用者</div>;
return (
<div>
<h1>{user.name}</h1>
<p>{user.email}</p>
</div>
);
}
// 3. 事件處理
interface FormProps {
onSubmit: (data: { name: string; email: string }) => void;
}
function Form({ onSubmit }: FormProps) {
const [name, setName] = useState<string>('');
const [email, setEmail] = useState<string>('');
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
onSubmit({ name, email });
};
const handleNameChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setName(e.target.value);
};
return (
<form onSubmit={handleSubmit}>
<input
type="text"
value={name}
onChange={handleNameChange}
/>
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
/>
<button type="submit">提交</button>
</form>
);
}
常見錯誤與解決方案
// 1. 物件可能為 'null' 或 'undefined'
// ❌ 錯誤
function getLength(text: string | null) {
return text.length; // ❌ text 可能為 null
}
// ✅ 解決方法 1:null 檢查
function getLength(text: string | null) {
if (text === null) {
return 0;
}
return text.length; // ✅ OK
}
// ✅ 解決方法 2:可選串連
function getLength(text: string | null) {
return text?.length ?? 0;
}
// 2. 型別 'X' 不可指派給型別 'Y'
// ❌ 錯誤
interface User {
name: string;
age: number;
}
const user: User = {
name: "金哲洙"
// 缺少 age!
};
// ✅ 解決:新增所有屬性
const user: User = {
name: "金哲洙",
age: 25
};
// 3. 無法找到名稱 'X'
// ❌ 錯誤
console.log(process.env.API_URL); // ❌ 沒有 process 型別
// ✅ 解決:安裝 @types
// npm install --save-dev @types/node
// 4. 型別 'Y' 上不存在屬性 'X'
// ❌ 錯誤
const obj = {};
obj.name = "金哲洙"; // ❌ 沒有 name 屬性
// ✅ 解決方法 1:型別定義
interface Obj {
name: string;
}
const obj: Obj = { name: "" };
obj.name = "金哲洙"; // ✅ OK
// ✅ 解決方法 2:索引簽名
const obj: { [key: string]: string } = {};
obj.name = "金哲洙"; // ✅ OK
// 5. 型別斷言
const input = document.getElementById('myInput');
// input 是 HTMLElement | null
// ❌ 錯誤
input.value = "hello"; // HTMLElement 沒有 value
// ✅ 解決:型別斷言
const input = document.getElementById('myInput') as HTMLInputElement;
input.value = "hello"; // ✅ OK
// 或
const input = <HTMLInputElement>document.getElementById('myInput');
input.value = "hello"; // ✅ OK
// 6. 非空斷言運算子(!)
const element = document.getElementById('app');
// element 是 HTMLElement | null
// ❌ 錯誤
element.innerHTML = "hello"; // 可能為 null
// ✅ 解決:null 檢查
if (element) {
element.innerHTML = "hello";
}
// 或 ! 運算子(僅在確定時!)
element!.innerHTML = "hello"; // 斷言非 null
// 7. 型別縮小
function printValue(value: string | number) {
// ❌ 錯誤
// console.log(value.toUpperCase()); // number 沒有 toUpperCase
// ✅ 解決:使用 typeof 檢查型別
if (typeof value === "string") {
console.log(value.toUpperCase()); // ✅ OK(字串)
} else {
console.log(value.toFixed(2)); // ✅ OK(數字)
}
}
🤔 常見問題
Q1. 為什麼要使用 TypeScript?
A: 有幾個原因:
// 1. 預防錯誤
// JavaScript
function calculateTotal(items) {
let total = 0;
items.forEach(item => {
total += item.price;
});
return total;
}
// 執行時發現的錯誤:
calculateTotal(null); // 💥 Runtime Error!
calculateTotal([{ name: "商品" }]); // 💥 price 為 undefined
// TypeScript
interface Item {
name: string;
price: number;
}
function calculateTotal(items: Item[]): number {
let total = 0;
items.forEach(item => {
total += item.price;
});
return total;
}
// 在程式碼撰寫時發現:
calculateTotal(null); // ❌ Compile Error!
calculateTotal([{ name: "商品" }]); // ❌ Compile Error!(沒有 price)
// 2. 自動完成和 IntelliSense
const user = {
name: "金哲洙",
age: 25,
email: "kim@example.com"
};
// JavaScript:輸入 user. 時無自動完成
// TypeScript:輸入 user. 時可自動完成 name、age、email!
// 3. 安全重構
// 修改函式簽名時
function greet(name) {
return `Hello, ${name}!`;
}
// TypeScript 會在所有調用處標記錯誤
// JavaScript 只有在執行時才發現問題
// 4. 起到文件作用
// JavaScript
function createUser(name, age, email, isAdmin) {
// 不知道 name 是字串還是數字
// 不知道 age 是必填還是選填
// 不知道 isAdmin 是什麼
}
// TypeScript
interface CreateUserParams {
name: string;
age: number;
email: string;
isAdmin?: boolean; // 選填,預設為 false
}
function createUser(params: CreateUserParams): User {
// 一切都很清楚!
}
// 5. 團隊協作
// 查看 TypeScript 程式碼:
// - 函式接收什麼參數
// - 回傳什麼型別
// - 有什麼屬性
// 一切都很清楚!
// 結論:
// JavaScript:快速原型、簡單腳本
// TypeScript:大型專案、團隊協作、維護
Q2. TypeScript 沒有缺點嗎?
A: 確實有一些缺點:
// 缺點 1:學習曲線
// 只懂 JavaScript 就可以立即開始
// TypeScript 需要學習型別系統
// 缺點 2:初始設定
// JavaScript:建立檔案就可以立即執行
// TypeScript:需要設定檔、編譯
// tsconfig.json
{
"compilerOptions": {
"target": "ES2020",
"module": "commonjs",
"strict": true,
// ... 許多選項
}
}
// 缺點 3:型別撰寫時間
// JavaScript
const users = [
{ name: "金哲洙", age: 25 },
{ name: "李英姬", age: 30 }
];
// TypeScript(需要定義型別)
interface User {
name: string;
age: number;
}
const users: User[] = [
{ name: "金哲洙", age: 25 },
{ name: "李英姬", age: 30 }
];
// 缺點 4:第三方函式庫
// 沒有型別定義的函式庫需要使用 any
import someLib from 'old-library'; // 無型別
// 需要安裝 @types/old-library
// 缺點 5:編譯時間
// 大型專案需要編譯時間
// (但已透過 esbuild、SWC 大幅改善)
// 但優點遠大於缺點!
// 特別是大型專案絕對必要!
Q3. 可以使用 any 嗎?
A: 盡量避免:
// ❌ 濫用 any(失去 TypeScript 優勢)
function processData(data: any) {
return data.map((item: any) => {
return {
id: item.id,
value: item.value
};
});
}
// ✅ 使用適當的型別
interface DataItem {
id: number;
value: string;
}
function processData(data: DataItem[]) {
return data.map(item => {
return {
id: item.id,
value: item.value
};
});
}
// 可以使用 any 的情況:
// 1. 第三方函式庫(無型別定義)
import oldLib from 'very-old-library';
const result: any = oldLib.doSomething();
// 2. JSON 解析(結構未知)
const data: any = JSON.parse(jsonString);
// 之後使用型別衛士驗證
// 3. 遷移舊程式碼
// JavaScript → TypeScript 漸進式轉換
// 替代方案:
// - unknown:比 any 更安全
// - 泛型:保留型別
// - 型別衛士:執行階段驗證
Q4. interface vs type,何時使用哪個?
A: 視情況而定:
// 使用 Interface(推薦)
// 1. 定義物件結構
interface User {
id: number;
name: string;
}
// 2. 可擴充
interface Student extends User {
studentId: string;
}
// 3. 宣告合併
interface Window {
myCustomProperty: string;
}
interface Window {
anotherProperty: number;
}
// 兩個宣告會合併!
// 使用 Type
// 1. 聯集型別
type Status = "pending" | "approved" | "rejected";
// 2. 交集型別
type Employee = Person & Worker;
// 3. 函式型別
type GreetFunction = (name: string) => string;
// 4. 實用型別
type PartialUser = Partial<User>;
type PickedUser = Pick<User, "id" | "name">;
// 5. 元組
type Point = [number, number];
// 6. 映射型別
type ReadonlyUser = {
readonly [K in keyof User]: User[K];
};
// 建議:
// - 預設使用 interface
// - 需要聯集/交集時使用 type
// - 維持專案內的一致性
Q5. 如何開始 TypeScript 專案?
A: 有多種方法:
# 1. Create React App(TypeScript 範本)
npx create-react-app my-app --template typescript
# 2. Next.js(TypeScript)
npx create-next-app@latest my-app --typescript
# 3. Vite(TypeScript)
npm create vite@latest my-app -- --template react-ts
# 4. 新增 TypeScript 到現有專案
npm install --save-dev typescript @types/react @types/react-dom
# 產生 tsconfig.json
npx tsc --init
# 5. 純 TypeScript 專案
mkdir my-project
cd my-project
npm init -y
npm install --save-dev typescript @types/node
# 產生 tsconfig.json
npx tsc --init
# 建立 src/index.ts 檔案
echo 'console.log("Hello TypeScript!");' > src/index.ts
# 編譯
npx tsc
# 執行
node dist/index.js
// tsconfig.json(推薦設定)
{
"compilerOptions": {
// 目標 JavaScript 版本
"target": "ES2020",
// 模組系統
"module": "ESNext",
"moduleResolution": "node",
// JSX 支援(React)
"jsx": "react-jsx",
// 嚴格型別檢查
"strict": true,
"noImplicitAny": true,
"strictNullChecks": true,
// 模組解析
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"resolveJsonModule": true,
// 輸出目錄
"outDir": "./dist",
// 原始碼映射
"sourceMap": true,
// 跳過不必要檢查
"skipLibCheck": true,
// 路徑別名
"baseUrl": ".",
"paths": {
"@/*": ["src/*"],
"@components/*": ["src/components/*"]
}
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}
🎓 下一步
學習完 TypeScript 基礎後,試試:
- 什麼是 React?(文件開發中)- 使用 TypeScript 開發 React
- 什麼是打包工具? - TypeScript 建置環境
- 什麼是 TDD?(文件開發中)- 使用 TypeScript 撰寫測試
🎬 總結
TypeScript 讓 JavaScript 更安全、更高效:
- 型別安全性:預防錯誤
- 自動完成:提升開發速度
- 重構:安全的程式碼變更
- 文件化:程式碼即文件
TypeScript 對大型專案和團隊協作至關重要。雖然有學習曲線,但投資絕對值得!