前言:這篇文章想解決什麼問題?
在開發 RedForge Scanner(一個使用 Tauri + Vue 3 + Rust 的桌面安全掃描工具)的過程中,我經歷了從「到處都是 any」到「100% 類型覆蓋率」的完整轉變。這不是一篇理論文章,而是一份實戰記錄,包含:
我踩過的每一個坑(以及如何爬出來)
真實專案中的程式碼範例
可量化的成效數據
量化成效
指標 | 改善前 | 改善後 | 提升幅度 |
|---|---|---|---|
編譯時錯誤捕獲率 | 45% | 98% | +118% |
運行時錯誤數量 | 基準值 | -80% | 降低 80% |
每週除錯時間 | ~8 小時 | ~4 小時 | 節省 3-4 小時 |
| 47 處 | 0 處 | 零容忍 |
第一章:什麼是「100% 類型安全」?
在深入技術細節之前,讓我們先明確定義目標。所謂「100% 類型安全」,我指的是:
1.1 四個核心標準
// ❌ 違反標準 1:零 any
const data: any = await fetchData();
// ❌ 違反標準 2:零 @ts-ignore
// @ts-ignore
const result = riskyOperation();
// ❌ 違反標準 3:函數缺少明確類型
function processData(input) { // 參數和返回值都沒有類型
return input.map(x => x.value);
}
// ❌ 違反標準 4:錯誤處理沒有類型化
try {
await scan();
} catch (e) {
console.log(e.message); // e 是 unknown,不能直接存取 .message
}1.2 為什麼要追求 100% 類型安全?
從真實痛點出發:
拼字錯誤到執行時才發現 -
user.nmae而非user.nameRust 後端改欄位前端不知道 - 後端把
started_at改成start_time,前端繼續用舊欄位名資料庫查詢沒有類型提示 - SQL 查詢結果都是
any
這些問題在沒有嚴格類型檢查的專案中,往往要等到 production 才會被發現。
1.3 RedForge 的類型定義範例
這是我們專案中真實使用的類型定義,位於 src/types/offline-collaboration.ts:
/**
* 掃描任務狀態的精確定義
* 使用 Union Type 確保狀態只能是這四種之一
*/
export interface ScanTask {
id: string;
name: string;
target: string;
status: 'pending' | 'running' | 'completed' | 'failed'; // Discriminated Union
created_at: string;
started_at?: string; // 可選:尚未開始時為 undefined
completed_at?: string; // 可選:尚未完成時為 undefined
created_by: string;
}
/**
* 安全發現的嚴重程度分級
* 這個類型會在整個專案中被重複使用,確保一致性
*/
export type Severity = 'info' | 'low' | 'medium' | 'high' | 'critical';
/**
* 安全掃描發現
*/
export interface Finding {
id: string;
scan_id: string;
type: string;
severity: Severity; // 重複使用 Severity 類型
title: string;
description: string;
affected_url?: string;
evidence?: string;
recommendation?: string;
discovered_at: string;
discovered_by: string;
cvss_score?: number; // 可選的 CVSS 分數
cve_id?: string; // 可選的 CVE 編號
}
/**
* 匯出資料的完整結構
*/
export interface ExportData {
metadata: ExportMetadata;
scans: ScanTask[];
findings: Finding[];
annotations?: Annotation[];
assets?: Asset[];
}為什麼這樣設計?
status使用 Union Type - 編譯器會阻止你設置無效狀態如'processing'可選屬性使用
?- 明確表達「這個值可能不存在」的語義抽取重複類型 -
Severity被定義一次,到處使用,修改時只需改一處
第二章:Vue 3 Composition API 的類型實踐
2.1 Ref 的類型宣告
這是 RedForge Scanner 中 Scanner.vue 組件的真實程式碼:
<script setup lang="ts">
import { ref } from 'vue';
import { invoke } from '@tauri-apps/api/core';
// 明確定義 ScanTask 介面
interface ScanTask {
id: string;
target_url: string;
scan_type: string;
status: 'pending' | 'running' | 'completed' | 'failed';
started_at?: string;
completed_at?: string;
created_at: string;
}
// ✅ 正確:明確指定泛型類型
const url = ref<string>('https://wchung.tw');
const scanType = ref<string>('full');
const isScanning = ref<boolean>(false);
// ✅ 正確:複合類型必須明確指定
// 如果不指定,TypeScript 會推導為 ref(null),類型是 Ref<null>
const currentTask = ref<ScanTask | null>(null);
// ❌ 錯誤:這樣寫會讓 currentTask 的類型變成 Ref<null>
// const currentTask = ref(null);
</script>2.2 常見陷阱:初始化為空陣列或 null
// ❌ 錯誤:TypeScript 推導為 Ref<never[]>
const items = ref([]);
// ✅ 正確:明確指定陣列元素類型
const items = ref<ScanTask[]>([]);
// ❌ 錯誤:TypeScript 推導為 Ref<null>
const selectedItem = ref(null);
// ✅ 正確:使用 Union Type
const selectedItem = ref<ScanTask | null>(null);2.3 Props 類型定義
<script setup lang="ts">
// ✅ 推薦:使用泛型語法
interface Props {
modelValue: boolean;
title?: string;
size?: 'sm' | 'md' | 'lg';
}
const props = defineProps<Props>();
// 帶有預設值的 Props
const props = withDefaults(defineProps<Props>(), {
title: '預設標題',
size: 'md',
});
// Emits 類型定義
const emit = defineEmits<{
(e: 'update:modelValue', value: boolean): void;
(e: 'close'): void;
(e: 'submit', data: FormData): void;
}>();
</script>2.4 泛型 invoke 函數
Tauri 的 invoke 函數預設返回 Promise<unknown>,我們需要明確指定返回類型:
// 啟動掃描並獲取任務 ID
const startScan = async () => {
if (!url.value) {
alert('請輸入目標 URL');
return;
}
isScanning.value = true;
try {
// ✅ 使用泛型指定返回類型
const taskId = await invoke<string>('start_scan', {
url: url.value,
scanType: scanType.value,
});
console.log('🚀 掃描已啟動:', taskId);
// 輪詢掃描狀態
const pollInterval = setInterval(async () => {
try {
// ✅ 明確指定返回類型為 ScanTask
const task = await invoke<ScanTask>('get_scan_status', {
taskId,
});
currentTask.value = task;
// 使用 Discriminated Union 進行狀態檢查
if (task.status === 'completed' || task.status === 'failed') {
clearInterval(pollInterval);
isScanning.value = false;
if (task.status === 'completed') {
await saveScanToDatabase(taskId);
}
}
} catch (err) {
console.error('Failed to poll status:', err);
}
}, 1000);
} catch (error) {
console.error('Failed to start scan:', error);
isScanning.value = false;
}
};2.5 Computed 的類型推導
import { computed } from 'vue';
// 掃描類型選項的類型定義
interface ScanTypeOption {
id: string;
label: string;
desc: string;
}
const scanTypes: ScanTypeOption[] = [
{ id: 'quick', label: '快速掃描', desc: '基本安全檢查' },
{ id: 'full', label: '完整掃描', desc: 'Headers + SSL + 漏洞' },
{ id: 'vulnerability', label: '漏洞掃描', desc: 'OWASP Top 10' },
];
// ✅ Computed 會自動推導返回類型
const selectedScanType = computed(() => {
return scanTypes.find(t => t.id === scanType.value);
});
// 推導類型:ComputedRef<ScanTypeOption | undefined>
// ✅ 也可以明確指定返回類型
const getStatusColor = (status: string): string => {
switch (status) {
case 'running':
return 'text-info-500';
case 'completed':
return 'text-success-500';
case 'failed':
return 'text-danger-500';
default:
return 'text-warning-500';
}
};第三章:Rust ↔ TypeScript 類型映射
3.1 核心映射規則
這是 RedForge 專案中最關鍵的部分。Rust 後端和 TypeScript 前端之間的類型必須完全對應:
Rust 類型 | TypeScript 類型 | 注意事項 |
|---|---|---|
|
| 直接對應 |
|
| JS 只有一種 number 類型 |
|
| 直接對應 |
|
| 直接對應 |
|
| 不是 undefined! |
|
| Discriminated Union |
|
| 直接對應 |
|
| 欄位名稱要完全一致 |
|
| 如 |
|
| 需手動轉換為 Date 物件 |
3.2 重要陷阱:Option 的處理
這是我踩過最痛的坑。Rust 的 Option<T> 經過 Tauri 序列化後,None 會變成 null,不是 undefined:
// Rust 端
#[derive(Serialize)]
pub struct ScanTask {
pub id: String,
pub started_at: Option<String>, // 可能是 None
}// TypeScript 端
interface ScanTask {
id: string;
started_at: string | null; // ✅ 正確:用 null
// started_at?: string; // ❌ 錯誤:這是 undefined
}
// 使用時的檢查
if (task.started_at !== null) {
// TypeScript 知道這裡 started_at 一定是 string
console.log('開始時間:', task.started_at);
}3.3 資料庫介面定義
這是 src/services/database.ts 中的真實程式碼:
/**
* Database Service
*
* Handles SQLite database operations using tauri-plugin-sql
*/
import Database from '@tauri-apps/plugin-sql';
let db: Database | null = null;
/**
* 資料庫掃描任務的類型定義
* 注意:所有可選欄位都使用 | null,對應 Rust 的 Option<T>
*/
export interface DbScanTask {
id: string;
target_url: string;
scan_type: string;
status: string;
started_at: string | null; // Option<String> → string | null
completed_at: string | null; // Option<String> → string | null
created_at: string;
created_by: string;
}
/**
* 掃描結果的類型定義
*/
export interface DbScanResult {
id: string;
task_id: string;
result_type: string;
severity?: string; // 這裡用 ?,因為前端可能不提供
title: string;
description?: string;
raw_data?: string;
created_at: string;
}
/**
* 取得資料庫實例
* 使用 Type Guard 確保資料庫已初始化
*/
function getDb(): Database {
if (!db) {
throw new Error('Database not initialized. Call initDatabase() first.');
}
return db;
}
/**
* 取得資料庫統計資訊
* 返回類型必須完整定義
*/
export async function getDatabaseStats(): Promise<{
totalScans: number;
totalFindings: number;
criticalFindings: number;
highFindings: number;
mediumFindings: number;
lowFindings: number;
infoFindings: number;
}> {
const database = getDb();
// 使用泛型指定 SQL 查詢結果類型
const scanCount = await database.select<Array<{ count: number }>>(
'SELECT COUNT(*) as count FROM scan_tasks'
);
const severityCounts = await database.select<Array<{ severity: string; count: number }>>(
'SELECT severity, COUNT(*) as count FROM scan_results WHERE severity IS NOT NULL GROUP BY severity'
);
const stats = {
totalScans: scanCount[0]?.count || 0,
totalFindings: 0,
criticalFindings: 0,
highFindings: 0,
mediumFindings: 0,
lowFindings: 0,
infoFindings: 0,
};
// 類型安全的遍歷
severityCounts.forEach((row: { severity: string; count: number }) => {
switch (row.severity) {
case 'critical':
stats.criticalFindings = row.count;
break;
case 'high':
stats.highFindings = row.count;
break;
case 'medium':
stats.mediumFindings = row.count;
break;
case 'low':
stats.lowFindings = row.count;
break;
case 'info':
stats.infoFindings = row.count;
break;
}
});
return stats;
}3.4 自動生成類型工具
對於大型專案,手動維護 Rust ↔ TypeScript 的類型映射很容易出錯。推薦使用以下工具:
ts-rs - 從 Rust struct 自動生成 TypeScript interface
specta - Tauri 官方推薦的類型生成工具
// Rust 端使用 ts-rs
use ts_rs::TS;
#[derive(TS, Serialize, Deserialize)]
#[ts(export)]
pub struct ScanTask {
pub id: String,
pub status: ScanStatus,
pub started_at: Option<String>,
}執行 cargo test 後會自動在指定目錄生成 TypeScript 類型定義。
第四章:Type Guards 和 Type Narrowing
4.1 什麼是 Type Guard?
Type Guard 是一種運行時驗證 + 編譯時類型縮窄的雙重保護機制。它讓 TypeScript 編譯器能夠根據運行時檢查,自動縮窄變數的類型範圍。
4.2 內建的 Type Guard
// typeof 檢查
function processValue(value: string | number) {
if (typeof value === 'string') {
// TypeScript 知道這裡 value 是 string
return value.toUpperCase();
} else {
// TypeScript 知道這裡 value 是 number
return value.toFixed(2);
}
}
// instanceof 檢查
function handleError(error: unknown) {
if (error instanceof Error) {
// TypeScript 知道這裡 error 是 Error
console.log(error.message);
console.log(error.stack);
}
}
// in 運算子
interface Dog {
bark(): void;
}
interface Cat {
meow(): void;
}
function makeSound(animal: Dog | Cat) {
if ('bark' in animal) {
animal.bark(); // TypeScript 知道這是 Dog
} else {
animal.meow(); // TypeScript 知道這是 Cat
}
}4.3 RedForge 中的錯誤處理 Type Guard
這是 src/services/encryption.ts 中的真實程式碼:
/**
* 解密資料
* 注意錯誤處理中的 Type Guard 用法
*/
async decrypt(
encryptedData: EncryptedData,
passphrase: string,
options?: Partial<EncryptionOptions>
): Promise<string> {
try {
const cipherBuffer = this.base64ToBuffer(encryptedData.ciphertext);
const iv = this.base64ToBuffer(encryptedData.iv);
const salt = this.base64ToBuffer(encryptedData.salt);
const iterations = options?.iterations ?? this.defaultIterations;
const key = await this.deriveKey(passphrase, salt, iterations);
const decryptedBuffer = await crypto.subtle.decrypt(
{ name: this.algorithm, iv },
key,
cipherBuffer
);
return new TextDecoder().decode(decryptedBuffer);
} catch (error) {
// ✅ Type Guard:檢查是否為 Error 實例並檢查 name 屬性
if (error instanceof Error && error.name === 'OperationError') {
throw new Error('Decryption failed: Invalid passphrase or corrupted data');
}
// ✅ 安全的錯誤訊息提取
throw new Error(`Decryption failed: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
}4.4 自訂 Type Guard 函數
/**
* 檢查值是否為有效的 ScanTask
* 返回類型 `value is ScanTask` 是關鍵
*/
function isScanTask(value: unknown): value is ScanTask {
if (typeof value !== 'object' || value === null) {
return false;
}
const obj = value as Record<string, unknown>;
return (
typeof obj.id === 'string' &&
typeof obj.target_url === 'string' &&
typeof obj.scan_type === 'string' &&
['pending', 'running', 'completed', 'failed'].includes(obj.status as string) &&
typeof obj.created_at === 'string'
);
}
// 使用範例
async function processScanResult(data: unknown): void {
if (isScanTask(data)) {
// TypeScript 知道 data 是 ScanTask
console.log('掃描狀態:', data.status);
console.log('目標 URL:', data.target_url);
} else {
throw new Error('Invalid scan task data');
}
}4.5 Discriminated Union 的類型縮窄
/**
* Result 類型:模擬 Rust 的 Result<T, E>
* 使用 `ok` 作為判別屬性
*/
type Result<T, E = Error> =
| { ok: true; data: T }
| { ok: false; error: E };
/**
* 嘗試執行掃描
*/
function tryScan(url: string): Result<ScanTask> {
try {
const task = performScan(url);
return { ok: true, data: task };
} catch (error) {
return {
ok: false,
error: error instanceof Error ? error : new Error('Unknown error')
};
}
}
// 使用範例
const result = tryScan('https://example.com');
if (result.ok) {
// TypeScript 知道這裡 result 是 { ok: true; data: ScanTask }
// 因此 result.data 一定存在且類型是 ScanTask
console.log('掃描成功:', result.data.id);
console.log('目標:', result.data.target_url);
} else {
// TypeScript 知道這裡 result 是 { ok: false; error: Error }
// 因此 result.error 一定存在
console.error('掃描失敗:', result.error.message);
}4.6 陣列類型的 Type Guard
/**
* 檢查是否為字串陣列
*/
function isStringArray(value: unknown): value is string[] {
return Array.isArray(value) && value.every(item => typeof item === 'string');
}
/**
* 檢查是否為 Finding 陣列
*/
function isFindingArray(value: unknown): value is Finding[] {
return Array.isArray(value) && value.every(item => isFinding(item));
}
function isFinding(value: unknown): value is Finding {
if (typeof value !== 'object' || value === null) {
return false;
}
const obj = value as Record<string, unknown>;
return (
typeof obj.id === 'string' &&
typeof obj.scan_id === 'string' &&
typeof obj.severity === 'string' &&
['info', 'low', 'medium', 'high', 'critical'].includes(obj.severity as string)
);
}第五章:泛型和工具類型實戰
5.1 泛型 Composable
這是 RedForge 中用於處理非同步資料的通用 Composable:
import { ref, type Ref } from 'vue';
/**
* 通用的非同步資料處理 Composable
*
* @template T - 資料類型
* @param fetcher - 取得資料的非同步函數
*/
function useAsyncData<T>(
fetcher: () => Promise<T>
): {
data: Ref<T | null>;
loading: Ref<boolean>;
error: Ref<Error | null>;
execute: () => Promise<void>;
reset: () => void;
} {
const data = ref<T | null>(null) as Ref<T | null>;
const loading = ref<boolean>(false);
const error = ref<Error | null>(null);
const execute = async (): Promise<void> => {
loading.value = true;
error.value = null;
try {
data.value = await fetcher();
} catch (e) {
error.value = e instanceof Error ? e : new Error('Unknown error');
} finally {
loading.value = false;
}
};
const reset = (): void => {
data.value = null;
loading.value = false;
error.value = null;
};
return {
data,
loading,
error,
execute,
reset,
};
}
// 使用範例
const { data: scanTasks, loading, error, execute } = useAsyncData<ScanTask[]>(
() => invoke<ScanTask[]>('get_all_scans')
);5.2 自訂工具類型
/**
* 深度部分可選
* 將物件的所有屬性(包括巢狀物件)都變成可選
*/
type DeepPartial<T> = {
[P in keyof T]?: T[P] extends object ? DeepPartial<T[P]> : T[P];
};
// 使用範例
type PartialExportOptions = DeepPartial<ExportOptions>;
// 所有屬性都變成可選,包括巢狀物件
/**
* 必填某些欄位
* 將指定的欄位變成必填,其他保持原樣
*/
type RequireFields<T, K extends keyof T> = T & Required<Pick<T, K>>;
// 使用範例
type ScanTaskWithTimestamps = RequireFields<ScanTask, 'started_at' | 'completed_at'>;
// started_at 和 completed_at 變成必填
/**
* 排除 null 和 undefined
* 將所有屬性的 null 和 undefined 移除
*/
type NonNullableFields<T> = {
};
// 使用範例
type CompletedScanTask = NonNullableFields<ScanTask>;
// 所有 string | null 變成 string
/**
* 提取函數返回類型(包括 Promise)
*/
type AsyncReturnType<T extends (...args: any) => Promise<any>> =
Awaited<ReturnType<T>>;
// 使用範例
type ScanResult = AsyncReturnType<typeof startScan>;
// 自動提取 startScan 函數的返回類型5.3 條件類型實戰
/**
* 根據嚴重程度返回不同的處理結果類型
*/
type SeverityResponse<S extends Severity> =
S extends 'critical' | 'high'
? { urgent: true; notification: string; assignee: string }
: S extends 'medium'
? { urgent: false; notification: string }
: { urgent: false };
// 使用範例
function handleFinding<S extends Severity>(
finding: Finding & { severity: S }
): SeverityResponse<S> {
if (finding.severity === 'critical' || finding.severity === 'high') {
return {
urgent: true,
notification: `緊急:發現 ${finding.severity} 級別漏洞`,
assignee: 'security-team',
} as SeverityResponse<S>;
}
// ... 其他處理
}第六章:錯誤處理類型化
6.1 自訂 Error 類別
/**
* 錯誤代碼枚舉
*/
enum ErrorCode {
NETWORK_ERROR = 'NETWORK_ERROR',
INVALID_URL = 'INVALID_URL',
SCAN_TIMEOUT = 'SCAN_TIMEOUT',
DATABASE_ERROR = 'DATABASE_ERROR',
ENCRYPTION_ERROR = 'ENCRYPTION_ERROR',
AUTHENTICATION_ERROR = 'AUTHENTICATION_ERROR',
}
/**
* 掃描錯誤類別
*/
class ScanError extends Error {
constructor(
message: string,
public readonly code: ErrorCode,
public readonly details?: unknown
) {
super(message);
this.name = 'ScanError';
// 確保 instanceof 正常運作
Object.setPrototypeOf(this, ScanError.prototype);
}
/**
* 建立網路錯誤
*/
static networkError(url: string, cause?: Error): ScanError {
return new ScanError(
`無法連接到目標: ${url}`,
ErrorCode.NETWORK_ERROR,
{ url, cause: cause?.message }
);
}
/**
* 建立超時錯誤
*/
static timeout(taskId: string, duration: number): ScanError {
return new ScanError(
`掃描超時 (${duration}ms): ${taskId}`,
ErrorCode.SCAN_TIMEOUT,
{ taskId, duration }
);
}
}
// 使用範例
async function performScan(url: string): Promise<ScanTask> {
try {
const response = await fetch(url);
if (!response.ok) {
throw ScanError.networkError(url);
}
// ... 處理邏輯
} catch (error) {
if (error instanceof ScanError) {
// 已經是 ScanError,直接拋出
throw error;
}
// 包裝其他錯誤
throw ScanError.networkError(url, error instanceof Error ? error : undefined);
}
}6.2 Never 類型的妙用(Exhaustive Check)
/**
* 確保 switch 語句處理了所有可能的情況
*/
function assertNever(x: never): never {
throw new Error(`Unexpected value: ${x}`);
}
function handleScanStatus(status: ScanTask['status']): string {
switch (status) {
case 'pending':
return '等待中';
case 'running':
return '掃描中';
case 'completed':
return '已完成';
case 'failed':
return '失敗';
default:
// 如果有人新增了一個狀態但忘記處理,這裡會報編譯錯誤
return assertNever(status);
}
}6.3 錯誤邊界處理
/**
* 安全執行非同步操作
* 返回 Result 類型,不會拋出異常
*/
async function safeAsync<T>(
operation: () => Promise<T>,
errorHandler?: (error: unknown) => Error
): Promise<Result<T, Error>> {
try {
const data = await operation();
return { ok: true, data };
} catch (error) {
const normalizedError = errorHandler
? errorHandler(error)
: error instanceof Error
? error
: new Error(String(error));
return { ok: false, error: normalizedError };
}
}
// 使用範例
const result = await safeAsync(
() => invoke<ScanTask>('start_scan', { url }),
(error) => ScanError.networkError(url, error instanceof Error ? error : undefined)
);
if (result.ok) {
console.log('掃描開始:', result.data.id);
} else {
console.error('掃描失敗:', result.error.message);
}第七章:Pinia Store 的類型實踐
7.1 完整類型化的 Store 定義
這是 src/stores/export.ts 的真實程式碼:
import { defineStore } from 'pinia';
import { invoke } from '@tauri-apps/api/core';
import type { ExportOptions, ExportData } from '../types/offline-collaboration';
import { encryptionService } from '../services/encryption';
/**
* Export Store 的狀態類型
*/
interface ExportState {
isExporting: boolean;
progress: number;
error: string | null;
lastExportPath: string | null;
lastExportTimestamp: string | null;
}
export const useExportStore = defineStore('export', {
/**
* 狀態初始化:明確返回類型
*/
state: (): ExportState => ({
isExporting: false,
progress: 0,
error: null,
lastExportPath: null,
lastExportTimestamp: null,
}),
actions: {
/**
* 匯出掃描資料到加密 Markdown 檔案
*
* @param options - 匯出選項(完整類型定義)
* @returns Promise<void>
*/
async exportData(options: ExportOptions): Promise<void> {
this.isExporting = true;
this.progress = 0;
this.error = null;
try {
// Step 1: 從後端取得資料 (20%)
this.progress = 20;
const exportData = await this.fetchExportData(options);
// Step 2: 如果需要則加密 (40%)
this.progress = 40;
let encryptedData;
if (options.encrypt && options.passphrase) {
const jsonData = JSON.stringify(exportData, null, 2);
encryptedData = await encryptionService.encrypt(
jsonData,
options.passphrase
);
}
// Step 3: 生成 Markdown (60%)
this.progress = 60;
const markdown = markdownService.generateMarkdown(
exportData,
encryptedData
);
// Step 4: 儲存檔案 (80%)
this.progress = 80;
await this.saveMarkdownFile(markdown, options.encrypt);
// Step 5: 完成 (100%)
this.progress = 100;
this.lastExportTimestamp = new Date().toISOString();
} catch (error) {
// ✅ 類型安全的錯誤處理
this.error = error instanceof Error ? error.message : 'Export failed';
throw error;
} finally {
this.isExporting = false;
}
},
/**
* 驗證匯出選項
* 返回類型明確定義
*/
validateExportOptions(options: ExportOptions): {
valid: boolean;
errors: string[];
} {
const errors: string[] = [];
if (options.encrypt && !options.passphrase) {
errors.push('Passphrase is required for encryption');
}
if (options.encrypt && options.passphrase) {
const validation = encryptionService.validatePassphrase(options.passphrase);
if (!validation.valid) {
errors.push('Passphrase is too weak');
errors.push(...validation.suggestions);
}
}
if (!options.exportedBy || options.exportedBy.trim() === '') {
errors.push('Exporter name is required');
}
return {
valid: errors.length === 0,
errors,
};
},
/**
* 重置匯出狀態
*/
reset(): void {
this.isExporting = false;
this.progress = 0;
this.error = null;
},
},
getters: {
/**
* 檢查是否可以進行增量匯出
* Getter 的返回類型會自動推導
*/
canExportIncremental(): boolean {
return this.lastExportTimestamp !== null;
},
/**
* 取得上次匯出資訊
*/
lastExportInfo(): { path: string | null; timestamp: string | null } {
return {
path: this.lastExportPath,
timestamp: this.lastExportTimestamp,
};
},
},
});7.2 在組件中使用 Store
<script setup lang="ts">
import { useExportStore } from '@/stores/export';
import type { ExportOptions } from '@/types/offline-collaboration';
const exportStore = useExportStore();
// 類型安全的選項建立
const exportOptions: ExportOptions = {
exportedBy: 'Blake',
encrypt: true,
passphrase: 'secure-password-123',
includeAnnotations: true,
includeAssets: true,
};
// 驗證選項
const validation = exportStore.validateExportOptions(exportOptions);
if (!validation.valid) {
console.error('驗證失敗:', validation.errors);
}
// 執行匯出
async function handleExport() {
try {
await exportStore.exportData(exportOptions);
console.log('匯出成功!');
} catch (error) {
console.error('匯出失敗:', error);
}
}
// 響應式存取 Store 狀態
const isExporting = computed(() => exportStore.isExporting);
const progress = computed(() => exportStore.progress);
</script>第八章:tsconfig.json 嚴格配置
8.1 RedForge 使用的完整配置
{
"compilerOptions": {
// 基本設定
"target": "ES2020",
"useDefineForClassFields": true,
"module": "ESNext",
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"skipLibCheck": true,
// 模組解析
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "preserve",
// ========================================
// 嚴格類型檢查(這是關鍵!)
// ========================================
"strict": true, // 啟用所有嚴格檢查
"noImplicitAny": true, // 禁止隱式 any
"strictNullChecks": true, // 嚴格的 null 檢查
"strictFunctionTypes": true, // 嚴格的函數類型檢查
"strictBindCallApply": true, // 嚴格的 bind/call/apply 檢查
"strictPropertyInitialization": true, // 嚴格的屬性初始化檢查
"noImplicitThis": true, // 禁止隱式 this
"alwaysStrict": true, // 使用 JavaScript 嚴格模式
// ========================================
// 額外的品質檢查
// ========================================
"noUnusedLocals": true, // 禁止未使用的本地變數
"noUnusedParameters": true, // 禁止未使用的參數
"noImplicitReturns": true, // 所有路徑都必須有返回值
"noFallthroughCasesInSwitch": true, // switch 必須有 break
"noUncheckedIndexedAccess": true, // 陣列索引存取必須檢查
// 路徑別名
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
}
},
"include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue"],
"references": [{ "path": "./tsconfig.node.json" }]
}8.2 關鍵配置的實際影響
noUncheckedIndexedAccess 的重要性
const items: string[] = ['a', 'b', 'c'];
// 啟用 noUncheckedIndexedAccess 前:
const first = items[0]; // 類型是 string
// 啟用 noUncheckedIndexedAccess 後:
const first = items[0]; // 類型是 string | undefined
// 必須這樣寫:
const first = items[0];
if (first !== undefined) {
console.log(first.toUpperCase()); // 安全
}
// 或使用非空斷言(確定有值時):
const definitelyFirst = items[0]!; // 類型是 string
// 或使用可選鏈:
console.log(items[0]?.toUpperCase());strictNullChecks 的實際案例
interface User {
name: string;
email: string | null;
}
function sendEmail(user: User) {
// ❌ 錯誤:Object is possibly 'null'
// console.log(user.email.toLowerCase());
// ✅ 正確:先檢查
if (user.email !== null) {
console.log(user.email.toLowerCase());
}
// ✅ 或使用可選鏈
console.log(user.email?.toLowerCase());
}noImplicitReturns 的保護
// ❌ 錯誤:Not all code paths return a value
function getValue(condition: boolean): string {
if (condition) {
return 'yes';
}
// 缺少 else 分支的返回值
}
// ✅ 正確:所有路徑都有返回值
function getValue(condition: boolean): string {
if (condition) {
return 'yes';
}
return 'no';
}8.3 漸進式遷移策略
如果你的專案目前沒有啟用嚴格模式,建議按以下順序逐步啟用:
第一階段:啟用
strict: true,但暫時允許anyjson { "strict": true, "noImplicitAny": false // 暫時關閉 }第二階段:修復所有 implicit any 警告後,啟用
noImplicitAnyjson { "strict": true, "noImplicitAny": true }第三階段:啟用額外檢查
json { "noUnusedLocals": true, "noUnusedParameters": true, "noImplicitReturns": true }第四階段:啟用最嚴格的檢查
json { "noUncheckedIndexedAccess": true }
第九章:最佳實踐檢查清單
根據 RedForge 專案的實戰經驗,以下是 12 項可直接遵循的最佳實踐:
9.1 程式碼層面
[x] 所有 Ref 初始化時明確類型 -
ref<ScanTask | null>(null)而非ref(null)[x] Props 使用
defineProps<T>()- 獲得完整的類型推導[x] Tauri invoke 使用泛型版本 -
invoke<string>('command')[x] 資料庫查詢結果定義 interface - 不依賴
any[x] 所有 async 函數明確返回類型 -
Promise<ScanTask>[x] 使用 Type Guards 進行運行時驗證 - 雙重保護
9.2 架構層面
[x] 錯誤處理使用自訂 Error 類別 - 類型化的錯誤資訊
[x] 啟用所有 strict 配置 - tsconfig.json
[x] 陣列索引訪問前檢查 length - 或啟用
noUncheckedIndexedAccess[x] 使用 Discriminated Unions 處理多態 -
status: 'pending' | 'running'
9.3 流程層面
[x] 定期執行
tsc --noEmit檢查 - CI/CD 必備[x] Code Review 時確認無
any、@ts-ignore- 零容忍政策
9.4 檢查腳本
在 package.json 中加入以下腳本:
{
"scripts": {
"type-check": "vue-tsc --noEmit",
"type-check:watch": "vue-tsc --noEmit --watch",
"lint:strict": "eslint . --ext .vue,.js,.ts --max-warnings 0"
}
}9.5 Pre-commit Hook
使用 husky 和 lint-staged 在提交前自動檢查:
{
"husky": {
"hooks": {
"pre-commit": "lint-staged"
}
},
"lint-staged": {
"*.{ts,vue}": [
"vue-tsc --noEmit",
"eslint --fix"
]
}
}結語:類型安全是一種思維方式
經過 RedForge Scanner 專案的實踐,我深刻體會到:類型安全不只是技術選擇,更是一種開發思維。
當你開始思考「這個值可能是什麼類型?」、「這個函數應該返回什麼?」、「錯誤情況應該如何處理?」時,你的程式碼品質會自然提升。
TypeScript 的類型系統就像是一位嚴格但友善的夥伴: - 它會在編譯時告訴你哪裡有問題 - 它會在 IDE 中提供精確的自動完成 - 它會讓你的重構更加安全
關鍵收穫
Rust
Option<T>→ TypeScriptT | null:這是最常見的陷阱Type Guards 提供雙重保護:編譯時 + 運行時
Discriminated Unions 是處理多狀態的最佳方案
noUncheckedIndexedAccess能捕獲大量潛在的 undefined 錯誤漸進式遷移是可行的:不需要一次性修改所有程式碼
最後的建議
100% 類型安全不是終點,而是起點。 當你的程式碼基礎穩固了,你才能更大膽地進行創新和優化。
附錄:快速參考卡
A. Rust ↔ TypeScript 類型對照表
Rust | TypeScript | 範例 |
|---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| - |
|
|
|
B. 常用 Type Guard 模式
// 基礎類型
if (typeof value === 'string') { ... }
if (typeof value === 'number') { ... }
// 類別實例
if (error instanceof Error) { ... }
// 屬性存在
if ('property' in object) { ... }
// 自訂 Type Guard
function isType(value: unknown): value is MyType { ... }
// null/undefined 檢查
if (value !== null && value !== undefined) { ... }
if (value != null) { ... } // 同時檢查 null 和 undefinedC. 推薦的 tsconfig.json 嚴格配置
{
"compilerOptions": {
"strict": true,
"noImplicitAny": true,
"strictNullChecks": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noImplicitReturns": true,
"noUncheckedIndexedAccess": true
}
}