TypeScript 基礎與進階使用指南

__Notes
TypeScriptJavaScriptProgrammingStatic TypingType System

什麼是 TypeScript?

TypeScript 是 JavaScript 的超集,添加了靜態型別定義,可以在開發階段捕獲錯誤,提高程式碼質量和可維護性。

  • 靜態型別檢查:在編譯時檢查型別錯誤,而不是運行時。
  • 更好的開發體驗:提供自動完成、型別推斷和錯誤提示。
  • 更好的程式碼組織:通過接口和型別定義,使程式碼結構更清晰。

TypeScript 基礎型別

TypeScript 提供了多種基礎型別,以下是常見的例子:

typescript
// 基本型別
let isDone: boolean = false;
let decimal: number = 6;
let color: string = 'blue';
let list: number[] = [1, 2, 3];
let tuple: [string, number] = ['hello', 10];
 
// 枚舉
enum Color {
    Red,
    Green,
    Blue,
}
let c: Color = Color.Green;
 
// Any 和 Unknown
let notSure: any = 4;
let uncertain: unknown = 4;
 
// Void, Null 和 Undefined
function warnUser(): void {
    console.log('This is a warning message');
}
let u: undefined = undefined;
let n: null = null;

any 與 unknown 的差異

TypeScript 中的 anyunknown 型別都可以接受任何值,但它們在型別安全性上有重要的區別:

any 型別

any 型別基本上會關閉 TypeScript 的型別檢查:

typescript
let anyValue: any = 10;
anyValue = 'hello';
anyValue = true;
 
// 以下操作不會產生編譯錯誤,但可能在運行時出錯
anyValue.foo.bar; // 不會報錯
anyValue(); // 不會報錯
anyValue.trim(); // 不會報錯

any 的問題:

  • 繞過了型別檢查
  • 可能導致運行時錯誤
  • 失去了 IDE 的自動完成和提示功能
  • 錯誤可能在程式的其他部分才被發現

unknown 型別

unknown 是型別安全的 any

typescript
let unknownValue: unknown = 10;
unknownValue = 'hello';
unknownValue = true;
 
// 以下操作會產生編譯錯誤
// unknownValue.foo.bar;  // 錯誤:對象的型別為 'unknown'
// unknownValue();        // 錯誤:對象的型別為 'unknown'
// unknownValue.trim();   // 錯誤:對象的型別為 'unknown'
 
// 必須先進行型別檢查或型別斷言
if (typeof unknownValue === 'string') {
    unknownValue.trim(); // 現在可以了
}
 
// 或使用型別斷言
const stringValue = unknownValue as string;
stringValue.trim(); // 可以,但如果實際不是字符串會在運行時出錯

unknown 的優點:

  • 需要先進行型別檢查或型別斷言才能使用
  • 強制開發者處理型別安全問題
  • 錯誤在編譯時就能被發現
  • 保留了型別安全性

何時使用 any 和 unknown

  • 使用 unknown 當

    • 你不確定變數的型別
    • 你想保持型別安全
    • 你計劃在使用前進行型別檢查
  • 使用 any 當

    • 你正在快速原型開發
    • 你正在遷移 JavaScript 程式碼到 TypeScript
    • 你確實需要繞過型別檢查(應該盡量避免)

實際應用示例

typescript
// 處理 API 響應
async function fetchData(url: string): Promise<unknown> {
    const response = await fetch(url);
    return response.json(); // 返回 unknown 而不是 any
}
 
// 使用時需要進行型別檢查
async function processUserData() {
    const data = await fetchData('/api/users');
 
    // 型別保護
    if (data && typeof data === 'object' && 'users' in data && Array.isArray(data.users)) {
        // 現在可以安全地使用 data.users
        return data.users.map((user) => user.name);
    }
 
    return [];
}

在這個例子中,使用 unknown 而不是 any 迫使我們處理可能的型別問題,使程式碼更加健壯。

Type vs Interface

TypeScript 提供了兩種定義型別的方式:typeinterface

使用 Interface

Interface 主要用於定義對象的形狀:

typescript
// 定義接口
interface User {
    id: number;
    name: string;
    email?: string; // 可選屬性
    readonly createdAt: Date; // 唯讀屬性
}
 
// 擴展接口
interface Employee extends User {
    department: string;
    salary: number;
}
 
// 函數接口
interface SearchFunction {
    (source: string, subString: string): boolean;
}
 
// 實現函數接口
const searchStr: SearchFunction = (src, sub) => src.includes(sub);

使用 Type

Type 可以用於定義聯合型別、交叉型別和其他複雜型別:

typescript
// 聯合型別
type ID = string | number;
 
// 交叉型別
type AdminUser = User & { permissions: string[] };
 
// 字面量型別
type Direction = 'up' | 'down' | 'left' | 'right';
 
// 函數型別
type ClickHandler = (event: MouseEvent) => void;
 
// 使用型別別名定義對象
type Point = {
    x: number;
    y: number;
};

何時使用 Type 或 Interface?

  • 使用 Interface 當

    • 你需要定義一個對象的形狀
    • 你需要擴展該型別
    • 你需要聲明合併(同名接口會自動合併)
  • 使用 Type 當

    • 你需要使用聯合型別或交叉型別
    • 你需要使用條件型別
    • 你需要使用映射型別
    • 你需要使用元組型別

TypeScript 泛型

泛型允許你創建可重用的組件,這些組件可以與多種型別一起工作:

typescript
// 泛型函數
function identity<T>(arg: T): T {
    return arg;
}
 
// 使用泛型函數
let output = identity<string>('myString');
// 或者讓 TypeScript 自動推斷型別
let output2 = identity('myString');
 
// 泛型箭頭函數
const getFirstElement = <T>(array: T[]): T | undefined => {
    return array[0];
};
 
// 泛型接口
interface GenericResponse<T> {
    data: T;
    status: number;
    message: string;
}
 
// 使用泛型接口
const userResponse: GenericResponse<User> = {
    data: { id: 1, name: 'John', createdAt: new Date() },
    status: 200,
    message: 'Success',
};

常用工具型別

TypeScript 提供了多種內置的工具型別,幫助你轉換和操作型別:

Partial<T>

將型別的所有屬性設為可選:

typescript
interface User {
    id: number;
    name: string;
    email: string;
}
 
// 所有屬性變為可選
type PartialUser = Partial<User>;
// 等同於:
// type PartialUser = {
//     id?: number;
//     name?: string;
//     email?: string;
// }
 
// 實際應用:更新用戶信息
function updateUser(userId: number, updates: Partial<User>) {
    // 只需提供要更新的字段
}
 
updateUser(1, { name: 'New Name' }); // 有效

Required<T>

將型別的所有屬性設為必需:

typescript
interface Config {
    host?: string;
    port?: number;
    secure?: boolean;
}
 
// 所有屬性變為必需
type RequiredConfig = Required<Config>;
// 等同於:
// type RequiredConfig = {
//     host: string;
//     port: number;
//     secure: boolean;
// }

Pick<T, K>

從型別 T 中選擇一組屬性 K:

typescript
interface User {
    id: number;
    name: string;
    email: string;
    password: string;
}
 
// 只選擇 id 和 name 屬性
type UserBasicInfo = Pick<User, 'id' | 'name'>;
// 等同於:
// type UserBasicInfo = {
//     id: number;
//     name: string;
// }
 
// 實際應用:API 響應過濾
function getUserPublicInfo(user: User): UserBasicInfo {
    return {
        id: user.id,
        name: user.name,
    };
}

Omit<T, K>

從型別 T 中排除一組屬性 K:

typescript
interface User {
    id: number;
    name: string;
    email: string;
    password: string;
}
 
// 排除 password 屬性
type UserPublicInfo = Omit<User, 'password'>;
// 等同於:
// type UserPublicInfo = {
//     id: number;
//     name: string;
//     email: string;
// }

Record<K, T>

創建一個具有指定鍵型別 K 和值型別 T 的對象型別:

typescript
// 使用字符串作為鍵,User 作為值
type UserRecord = Record<string, User>;
 
// 使用特定字符串字面量作為鍵
type Roles = 'admin' | 'user' | 'guest';
type RoleConfig = Record<Roles, { permissions: string[] }>;
 
// 實際應用:配置對象
const rolePermissions: RoleConfig = {
    admin: { permissions: ['read', 'write', 'delete'] },
    user: { permissions: ['read', 'write'] },
    guest: { permissions: ['read'] },
};

Exclude<T, U> 和 Extract<T, U>

Exclude<T, U> 從 T 中排除可分配給 U 的型別,而 Extract<T, U> 從 T 中提取可分配給 U 的型別:

typescript
type T0 = Exclude<'a' | 'b' | 'c', 'a'>; // 'b' | 'c'
type T1 = Extract<'a' | 'b' | 'c', 'a' | 'f'>; // 'a'
 
// 實際應用:過濾型別
type EventType = 'click' | 'scroll' | 'mousemove';
type MouseEventType = Extract<EventType, 'click' | 'mousemove'>; // 'click' | 'mousemove'

高級型別技巧

條件型別

根據條件選擇不同的型別:

typescript
type IsString<T> = T extends string ? true : false;
 
type A = IsString<string>; // true
type B = IsString<number>; // false
 
// 實際應用:根據輸入型別返回不同的輸出型別
type ReturnType<T> = T extends (...args: any[]) => infer R ? R : never;
 
function createUser() {
    return { id: 1, name: 'John' };
}
type User = ReturnType<typeof createUser>; // { id: number, name: string }

映射型別

通過映射現有型別的每個屬性來創建新型別:

typescript
type Readonly<T> = {
    readonly [P in keyof T]: T[P];
};
 
interface User {
    id: number;
    name: string;
}
 
type ReadonlyUser = Readonly<User>;
// 等同於:
// type ReadonlyUser = {
//     readonly id: number;
//     readonly name: string;
// }

實際應用示例

React 函數組件 Props 型別定義

typescript
import React from 'react';
 
// 使用 interface 定義 Props
interface ButtonProps {
    text: string;
    onClick: () => void;
    variant?: 'primary' | 'secondary';
    disabled?: boolean;
}
 
// 函數組件
const Button: React.FC<ButtonProps> = ({
    text,
    onClick,
    variant = 'primary',
    disabled = false
}) => {
    return (
        <button
            onClick={onClick}
            className={`btn btn-${variant}`}
            disabled={disabled}
        >
            {text}
        </button>
    );
};
 
// 使用組件
const App = () => {
    return (
        <Button
            text="Click me"
            onClick={() => console.log('Button clicked')}
            variant="primary"
        />
    );
};

使用 React Hooks 的型別定義

typescript
import React, { useState, useEffect } from 'react';
 
// 定義用戶型別
interface User {
    id: number;
    name: string;
    email: string;
}
 
// 使用泛型 useState
const UserProfile: React.FC = () => {
    // 使用泛型指定 state 型別
    const [user, setUser] = useState<User | null>(null);
    const [loading, setLoading] = useState<boolean>(true);
    const [error, setError] = useState<string | null>(null);
 
    useEffect(() => {
        const fetchUser = async () => {
            try {
                setLoading(true);
                const response = await fetch('/api/user/1');
                const data = await response.json();
                setUser(data);
            } catch (err) {
                setError(err instanceof Error ? err.message : 'Unknown error');
            } finally {
                setLoading(false);
            }
        };
 
        fetchUser();
    }, []);
 
    if (loading) return <div>Loading...</div>;
    if (error) return <div>Error: {error}</div>;
    if (!user) return <div>No user found</div>;
 
    return (
        <div>
            <h1>{user.name}</h1>
            <p>Email: {user.email}</p>
        </div>
    );
};

API 響應型別定義

typescript
// 基本用戶型別
interface User {
    id: number;
    name: string;
    email: string;
}
 
// API 響應型別
interface ApiResponse<T> {
    data: T;
    status: number;
    message: string;
    timestamp: string;
}
 
// 獲取用戶列表響應
type GetUsersResponse = ApiResponse<User[]>;
 
// 獲取單個用戶響應
type GetUserResponse = ApiResponse<User>;
 
// 使用示例
const fetchUsers = async (): Promise<User[]> => {
    try {
        const response = await fetch('/api/users');
        const result: GetUsersResponse = await response.json();
        return result.data;
    } catch (error) {
        console.error('Error fetching users:', error);
        return [];
    }
};

自定義 Hook 的型別定義

typescript
import { useState, useCallback } from 'react';
 
// 定義自定義 Hook 的返回型別
interface UseToggleReturn {
    value: boolean;
    toggle: () => void;
    setTrue: () => void;
    setFalse: () => void;
}
 
// 自定義 Hook
function useToggle(initialValue = false): UseToggleReturn {
    const [value, setValue] = useState<boolean>(initialValue);
 
    const toggle = useCallback(() => setValue(v => !v), []);
    const setTrue = useCallback(() => setValue(true), []);
    const setFalse = useCallback(() => setValue(false), []);
 
    return { value, toggle, setTrue, setFalse };
}
 
// 使用自定義 Hook
const ToggleComponent: React.FC = () => {
    const { value, toggle } = useToggle();
 
    return (
        <button onClick={toggle}>
            {value ? 'ON' : 'OFF'}
        </button>
    );
};

總結

TypeScript 提供了強大的型別系統,可以幫助你寫出更安全、更可維護的程式碼。通過使用適當的型別定義、泛型和工具型別,你可以大大提高開發效率和程式碼質量。

  • 基礎型別:為變數提供型別註解
  • Interface vs Type:根據需求選擇合適的型別定義方式
  • 泛型:創建可重用的型別安全組件
  • 工具型別:使用內置工具型別轉換和操作型別
  • 高級型別技巧:使用條件型別和映射型別解決複雜問題

參考來源