引言:從基礎到結構化類型
? ? ? ? 在《TypeScript 基礎介紹(一)》TypeScript基礎介紹(一)-CSDN博客中,我們探討了 TypeScript 的類型系統基礎、聯合類型、類型斷言和類型守衛等核心特性。這些內容解決了 JavaScript 在類型檢查和代碼可讀性方面的基礎問題。然而,隨著應用規模增長,我們需要更強大的工具來描述復雜對象結構、復用類型定義并實現類型安全的代碼復用。本文 fly 將繼續深入 TypeScript 的結構化類型系統,包括接口、類型別名、函數類型、泛型及類與接口的結合,通過實戰案例展示如何構建健壯且可維護的類型系統。
目錄
引言:從基礎到結構化類型
六、接口:定義對象的結構契約
6.1 基礎接口定義與使用
6.2 接口的索引簽名:動態屬性名
6.3 接口繼承:復用與擴展類型
七、類型別名:創建自定義類型
7.1 基礎類型別名
7.2 對象類型別名
八、函數類型:精確描述函數簽名
8.1 函數類型表達式
8.2 接口定義函數類型
8.3 函數參數的高級類型
九、泛型:編寫可復用的類型安全代碼
9.1 泛型函數:適應多種類型
9.2 泛型約束:限制類型范圍
9.3 泛型接口與類
十、類與接口:面向對象編程的類型約束
10.1 類實現接口
10.2 類的類型繼承與接口實現結合
十一、交叉類型:組合多個類型
十二、類型別名 vs 接口:何時選擇哪種方式
12.1 核心區別對比
12.2 最佳實踐建議
結語
六、接口:定義對象的結構契約
? ? ? ? 接口(Interface)是 TypeScript 中描述對象形狀的核心工具,它定義了對象必須包含的屬性和方法,是實現代碼契約化設計的基礎。與基本類型不同,接口專注于描述復雜數據結構,確保不同部分的代碼遵循一致的數據格式。
6.1 基礎接口定義與使用
? ? ? ?接口通過interface
關鍵字聲明,指定對象應包含的屬性名稱、類型及可選性:
// 定義用戶接口
interface User {id: number; // 必選屬性name: string; // 必選屬性age?: number; // 可選屬性(使用?標記)readonly email: string; // 只讀屬性(初始化后不可修改)
}// 正確實現接口
const validUser: User = {id: 1,name: "Alice",email: "alice@example.com"
};// 錯誤示例:缺少必選屬性id
const invalidUser: User = {name: "Bob",email: "bob@example.com"// ? 類型 "{ name: string; email: string; }" 中缺少屬性 "id",但類型 "User" 中需要該屬性
};// 錯誤示例:修改只讀屬性
validUser.email = "new-email@example.com";
// ? 無法分配到 "email" ,因為它是只讀屬性
關鍵特性:
- 必選屬性:默認情況下,接口屬性為必填,實現時必須提供
- 可選屬性:通過
?
標記,允許對象缺少該屬性(如age?: number
) - 只讀屬性:通過
readonly
關鍵字,確保屬性初始化后不可修改(運行時仍可通過索引修改,但編譯時會報錯)
6.2 接口的索引簽名:動態屬性名
? ? ? ? 當對象屬性名不確定但類型已知時,可使用索引簽名定義鍵值對的類型約束:
// 字符串索引簽名:鍵為string,值為number
interface NumberDictionary {[key: string]: number;length: number; // 允許,因為length是string類型鍵,值為number// name: string; // ? 錯誤,值類型必須為number
}// 數字索引簽名:鍵為number,值為string
interface StringArray {[index: number]: string;
}const fruits: StringArray = ["apple", "banana", "cherry"];
console.log(fruits[0]); // 輸出: "apple"(TypeScript推斷為string類型)
應用場景:處理 JSON 數據、配置對象等動態結構,同時保持類型安全。
6.3 接口繼承:復用與擴展類型
? ? ? ? 接口支持通過extends
關鍵字繼承其他接口,實現類型復用和擴展:
// 基礎接口
interface Person {name: string;age: number;
}// 繼承Person并添加職業屬性
interface Employee extends Person {department: string;salary: number;
}// 實現繼承后的接口
const employee: Employee = {name: "John",age: 30,department: "Engineering",salary: 80000
};// 多繼承:同時繼承多個接口
interface Contact {phone: string;
}interface Staff extends Person, Contact {id: number;
}const staff: Staff = {name: "Jane",age: 28,phone: "123-456-7890",id: 1001
};
優勢:避免代碼重復,構建層次化的類型體系,符合開閉原則(對擴展開放,對修改封閉)。
七、類型別名:創建自定義類型
? ? ? ? 類型別名(Type Alias)通過type
關鍵字為已有類型創建新名稱,支持基本類型、聯合類型、交叉類型等復雜場景,是定義復用類型的靈活工具。
7.1 基礎類型別名
? ? ? ? 為基本類型或聯合類型創建別名,提升代碼可讀性:
// 為基本類型創建別名
type Age = number;
type Name = string;// 為聯合類型創建別名
type Status = "active" | "inactive" | "pending";
type ID = string | number;// 使用類型別名
let userAge: Age = 25;
let userName: Name = "Alice";
let userStatus: Status = "active"; // 只能賦值指定的字符串字面量
let userId: ID = "user-123"; // 合法,string類型
userId = 456; // 合法,number類型// 錯誤示例:賦值不在聯合類型中的值
userStatus = "deleted";
// ? 類型 ""deleted"" 不能賦值給類型 "Status"
7.2 對象類型別名
? ? ? ? 與接口類似,類型別名可描述對象結構,但支持更復雜的組合:
// 對象類型別名
type Point = {x: number;y: number;z?: number; // 可選屬性
};// 聯合類型別名
type Shape = | { kind: "circle"; radius: number }| { kind: "square"; sideLength: number }| { kind: "triangle"; base: number; height: number };// 使用類型別名計算面積
function calculateArea(shape: Shape): number {switch (shape.kind) {case "circle":return Math.PI * shape.radius ** 2;case "square":return shape.sideLength ** 2;case "triangle":return (shape.base * shape.height) / 2;default:// exhaustive check( exhaustive:徹底的):確保覆蓋所有可能的類型const _exhaustiveCheck: never = shape;return _exhaustiveCheck;}
}// 示例調用
console.log(calculateArea({ kind: "circle", radius: 5 })); // 輸出: 78.5398...
console.log(calculateArea({ kind: "square", sideLength: 10 })); // 輸出: 100
關鍵特性:支持聯合類型、交叉類型等復雜組合,適合描述非對象類型(如基本類型、聯合類型)。
八、函數類型:精確描述函數簽名
? ? ? ? TypeScript 允許通過函數類型表達式或接口定義函數的參數和返回值類型,確保函數調用的類型安全。
8.1 函數類型表達式
? ? ? ? 直接在函數變量或參數中定義類型:
// 定義函數類型:(參數1: 類型, 參數2: 類型) => 返回值類型
type AddFunction = (a: number, b: number) => number;// 使用函數類型
const add: AddFunction = (a, b) => a + b;
console.log(add(2, 3)); // 輸出: 5// 錯誤示例:參數類型不匹配
const subtract: AddFunction = (a: string, b: number) => a - b;
// ? 不能將類型 "(a: string, b: number) => number" 分配給類型 "AddFunction"// 函數作為參數時的類型
function calculate(operation: AddFunction, x: number, y: number): number {return operation(x, y);
}console.log(calculate(add, 10, 20)); // 輸出: 30
8.2 接口定義函數類型
? ? ? ? ?通過接口的調用簽名描述函數結構,適合需要擴展屬性的函數:
// 接口定義函數類型(調用簽名)
interface GreetFunction {(name: string, greeting?: string): string; // 函數參數和返回值defaultGreeting: string; // 函數額外屬性
}// 實現接口
const greet: GreetFunction = (name, greeting = greet.defaultGreeting) => {return `${greeting}, ${name}!`;
};
greet.defaultGreeting = "Hello";// 調用函數
console.log(greet("Alice")); // 輸出: "Hello, Alice!"
console.log(greet("Bob", "Hi")); // 輸出: "Hi, Bob!"
8.3 函數參數的高級類型
? ? ? ? 詳細講解函數參數的類型細節,包括可選參數、默認參數、剩余參數:
// 可選參數:使用?標記,必須放在必選參數之后
function logUser(name: string, age?: number): void {console.log(`Name: ${name}, Age: ${age ?? "Unknown"}`);
}
logUser("Alice"); // 輸出: "Name: Alice, Age: Unknown"// 默認參數:指定默認值,自動成為可選參數
function createUser(name: string, role: string = "user"): { name: string; role: string } {return { name, role };
}
console.log(createUser("Bob")); // 輸出: { name: "Bob", role: "user" }// 剩余參數:使用...收集多個參數為數組
function sum(...numbers: number[]): number {return numbers.reduce((acc, curr) => acc + curr, 0);
}
console.log(sum(1, 2, 3, 4)); // 輸出: 10
? ? ? ? ?最佳實踐:為所有公共函數添加完整的類型注解,特別是參數和返回值類型,提升代碼可讀性和重構安全性。
九、泛型:編寫可復用的類型安全代碼
? ? ? ? 泛型(Generics)是 TypeScript 實現類型復用的核心機制,允許在定義函數、接口或類時不指定具體類型,而在使用時動態指定,實現 "一次定義,多種類型復用"。
9.1 泛型函數:適應多種類型
? ? ? ? 定義一個可處理不同類型數據的函數,同時保持類型安全:
// 泛型函數:T是類型變量,代表傳入的類型
function identity<T>(arg: T): T {return arg; // 返回與輸入相同類型的值
}// 使用泛型函數(顯式指定類型)
const num: number = identity<number>(42);
const str: string = identity<string>("hello");// 類型推斷(推薦):TypeScript自動推斷T為傳入的類型
const bool = identity(true); // T被推斷為boolean類型// 泛型函數示例:獲取數組第一個元素
function getFirstElement<T>(array: T[]): T | undefined {return array[0];
}// 使用示例
const numbers = [1, 2, 3];
const firstNum = getFirstElement(numbers); // 推斷為number | undefined
console.log(firstNum); // 輸出: 1const strings = ["a", "b", "c"];
const firstStr = getFirstElement(strings); // 推斷為string | undefined
9.2 泛型約束:限制類型范圍
? ? ? ? 使用extends
關鍵字約束泛型只能是特定類型或具有特定屬性:
// 泛型約束:T必須具有length屬性
interface Lengthwise {length: number;
}function logLength<T extends Lengthwise>(arg: T): T {console.log(`Length: ${arg.length}`);return arg;
}logLength("hello"); // 輸出: "Length: 5"(string有length屬性)
logLength([1, 2, 3]); // 輸出: "Length: 3"(數組有length屬性)
// logLength(42); // ? 錯誤,number沒有length屬性// 泛型約束:使用keyof獲取對象鍵的聯合類型
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {return obj[key];
}const user = { name: "Alice", age: 25 };
console.log(getProperty(user, "name")); // 輸出: "Alice"(類型為string)
console.log(getProperty(user, "age")); // 輸出: 25(類型為number)
// getProperty(user, "email"); // ? 錯誤,"email"不是user的鍵
9.3 泛型接口與類
? ? ? ? 將泛型應用于接口和類,創建可復用的類型組件:
// 泛型接口
interface Box<T> {value: T;getValue: () => T;
}// 實現泛型接口
const numberBox: Box<number> = {value: 100,getValue: () => numberBox.value
};const stringBox: Box<string> = {value: "TypeScript",getValue: () => stringBox.value
};// 泛型類
class Stack<T> {private items: T[] = [];push(item: T): void {this.items.push(item);}pop(): T | undefined {return this.items.pop();}
}// 使用泛型類
const numberStack = new Stack<number>();
numberStack.push(1);
numberStack.push(2);
console.log(numberStack.pop()); // 輸出: 2const stringStack = new Stack<string>();
stringStack.push("a");
stringStack.push("b");
console.log(stringStack.pop()); // 輸出: "b"
? ? ? ? 泛型的價值:在保持類型安全的同時,大幅提升代碼復用性,是開發通用庫和組件的基礎。
十、類與接口:面向對象編程的類型約束
? ? ? ? TypeScript 結合了面向對象編程和類型系統,允許通過接口約束類的實現,確保類遵循特定的結構。
10.1 類實現接口
? ? ? ? 使用implements
關鍵字使類遵循接口定義的結構:
// 定義接口
interface Animal {name: string;makeSound(): void;
}// 類實現接口
class Dog implements Animal {name: string;constructor(name: string) {this.name = name;}makeSound(): void {console.log("Woof!");}// 類可以有接口之外的方法fetch(): void {console.log(`${this.name} is fetching the ball`);}
}// 實例化類
const dog = new Dog("Buddy");
dog.makeSound(); // 輸出: "Woof!"
dog.fetch(); // 輸出: "Buddy is fetching the ball"// 錯誤示例:未實現接口的方法
class Cat implements Animal {name: string;constructor(name: string) {this.name = name;}// ? 錯誤,Cat類缺少"makeSound"方法的實現
}
10.2 類的類型繼承與接口實現結合
? ? ? ? 類可以同時繼承另一個類并實現接口,實現代碼復用和接口約束的雙重目的:
// 基礎類
class Vehicle {speed: number;constructor(speed: number) {this.speed = speed;}move(): void {console.log(`Moving at ${this.speed} km/h`);}
}// 接口
interface Flyable {altitude: number;fly(): void;
}// 繼承類并實現接口
class Airplane extends Vehicle implements Flyable {altitude: number;constructor(speed: number, altitude: number) {super(speed); // 調用父類構造函數this.altitude = altitude;}// 重寫父類方法move(): void {console.log(`Flying at ${this.speed} km/h and ${this.altitude} m altitude`);}// 實現接口方法fly(): void {console.log("Taking off!");}
}// 使用類
const plane = new Airplane(900, 10000);
plane.fly(); // 輸出: "Taking off!"
plane.move(); // 輸出: "Flying at 900 km/h and 10000 m altitude"
十一、交叉類型:組合多個類型
? ? ? ? 交叉類型(Intersection Types)使用&
符號將多個類型合并為一個,新類型包含所有類型的屬性和方法,適用于組合對象結構。
// 定義兩個接口
interface HasName {name: string;
}interface HasAge {age: number;
}// 交叉類型:同時包含HasName和HasAge的屬性
type Person = HasName & HasAge;// 使用交叉類型
const person: Person = {name: "Alice",age: 25
};// 交叉類型與聯合類型的區別
type A = { a: number } & { b: string }; // 必須同時有a和b
type B = { a: number } | { b: string }; // 可以有a或b或兩者都有// 復雜交叉類型示例
type WithId = { id: string };
type User = HasName & HasAge & WithId;const user: User = {id: "user-123",name: "Bob",age: 30
};
注意:交叉類型不適合基本類型組合(如string & number
會得到never
類型,因為沒有值同時是 string 和 number)。
十二、類型別名 vs 接口:何時選擇哪種方式
? ? ? ? 類型別名和接口都可用于定義對象結構,但在使用場景上有明確區別,選擇正確的工具能提升代碼清晰度和可維護性。
12.1 核心區別對比
特性 | 類型別名(type) | 接口(interface) |
---|---|---|
定義范圍 | 可描述任意類型(對象、聯合、基本類型等) | 主要用于描述對象結構和函數類型 |
擴展方式 | 通過交叉類型(type A = B & { ... } ) | 通過繼承(interface A extends B ) |
合并聲明 | 不支持重復聲明合并 | 支持重復聲明自動合并 |
計算屬性 | 支持(如type Keys = keyof T ) | 支持,但語法更復雜 |
12.2 最佳實踐建議
-
優先使用接口當:
- 定義對象結構且需要繼承或被繼承
- 需要自動合并聲明(如擴展第三方庫類型)
- 描述類的公共 API(更符合面向對象思維)
-
優先使用類型別名當:
- 定義聯合類型、交叉類型或基本類型別名
- 描述元組類型(如
type Point = [number, number]
) - 需要使用計算屬性或條件類型
// 接口合并示例(接口特有)
interface Config {apiUrl: string;
}interface Config {timeout: number;
}// 自動合并為 { apiUrl: string; timeout: number }
const config: Config = {apiUrl: "https://api.example.com",timeout: 5000
};// 類型別名不支持合并
type Settings = { theme: string };
// type Settings = { layout: string }; // ? 錯誤,重復的標識符"Settings"
結語
? ? ? ? 本文深入探討了 TypeScript 的結構化類型特性,包括接口、類型別名、函數類型、泛型、類與接口的結合及交叉類型等核心概念。這些工具共同構成了 TypeScript 強大的類型系統,使開發者能夠構建類型安全、可維護且高度復用的代碼。