TypeScript 泛型與類型操作詳解(一)
TypeScript 提供了強大的類型系統,其中泛型(Generics)和類型操作(Type Manipulation)是其核心特性之一。本文將詳細介紹 TypeScript 中的泛型及其相關概念,并通過案例代碼進行說明。
一、泛型簡介
泛型允許在定義函數、接口或類時,不預先指定具體的類型,而是在使用時指定類型。這種方式提高了代碼的復用性和靈活性。
為什么使用泛型
- 提高代碼重用性
- 提供更好的類型安全性
- 減少使用
any
類型的需要 - 在編譯時捕獲類型錯誤
1.1 形式類型參數與實際類型參數
- 形式類型參數(Type Parameters):在泛型定義中聲明的類型占位符,通常使用
<T>
、<T, U>
等形式。 - 實際類型參數(Type Arguments):在使用泛型時,傳入的具體類型。
示例:
// 定義一個泛型函數,形式類型參數為 T
function identity<T>(arg: T): T {return arg;
}// 使用泛型函數,實際類型參數為 number
let output = identity<number>(42); // output 的類型為 number// 也可以讓 TypeScript 推斷類型
let output2 = identity("Hello, TypeScript!"); // output2 的類型為 string
1.2 泛型約束
有時需要對泛型參數進行約束,限制其必須符合某些條件。可以使用 extends
關鍵字實現。
示例:
// 定義一個接口,描述必須具有 length 屬性的類型
interface Lengthwise {length: number;
}// 使用泛型約束,限制 T 必須符合 Lengthwise 接口
function loggingIdentity<T extends Lengthwise>(arg: T): T {console.log(arg.length); // 現在可以安全地使用 length 屬性return arg;
}// 正確使用
loggingIdentity("Hello"); // string 符合 Lengthwise 接口// 錯誤使用,會在編譯時報錯
// loggingIdentity(42); // number 不符合 Lengthwise 接口
二、泛型函數
泛型函數允許函數在調用時指定類型參數,從而實現更靈活的參數和返回值類型。
示例:
// 泛型函數,返回第一個元素
function getFirstElement<T>(arr: T[]): T {return arr[0];
}let firstNumber = getFirstElement<number>([1, 2, 3]); // firstNumber 的類型為 number
let firstString = getFirstElement<string>(["a", "b", "c"]); // firstString 的類型為 string
三、泛型接口
接口也可以是泛型的,用于定義泛型函數的類型或泛型對象的結構。
示例:
// 定義一個泛型接口,描述一個包含鍵值對的對象
interface Pair<K, V> {key: K;value: V;
}// 使用泛型接口
let user: Pair<string, number> = {key: "age",value: 30
};
四、泛型類型別名
類型別名可以使用泛型來創建可復用的復雜類型。
示例:
// 定義一個泛型類型別名,描述一個數組或對象
type Container<T> = T[] | { value: T };// 使用泛型類型別名
let numberContainer: Container<number> = [1, 2, 3];
let stringContainer: Container<string> = { value: "Hello" };
五、泛型類
類也可以是泛型的,用于創建可復用的組件。
示例:
// 定義一個泛型類,表示一個棧
class Stack<T> {private elements: T[] = [];push(element: T) {this.elements.push(element);}pop(): T | undefined {return this.elements.pop();}peek(): T | undefined {return this.elements[this.elements.length - 1];}
}// 使用泛型類
let numberStack = new Stack<number>();
numberStack.push(1);
numberStack.push(2);
console.log(numberStack.peek()); // 輸出: 2let stringStack = new Stack<string>();
stringStack.push("a");
stringStack.push("b");
console.log(stringStack.peek()); // 輸出: b
六、局部類型
局部類型是指在函數或塊作用域內定義的類型。
示例:
function process<T>(value: T) {// 定義一個局部類型type Wrapped = { wrapped: T };let wrappedValue: Wrapped = { wrapped: value };console.log(wrappedValue);
}process("Hello"); // 輸出: { wrapped: "Hello" }
七、聯合類型
聯合類型表示一個值可以是幾種類型之一,使用 |
分隔。
示例:
function printId(id: number | string) {console.log("ID:", id);
}printId(101); // 輸出: ID: 101
printId("202"); // 輸出: ID: 202
7.1 聯合類型字面量
聯合類型字面量是指字面量值的聯合。
示例:
type Direction = "left" | "right" | "up" | "down";function move(direction: Direction) {console.log("Moving", direction);
}move("left"); // 輸出: Moving left
// move("forward"); // 編譯錯誤
7.2 聯合類型的類型成員
聯合類型的每個成員類型必須符合所有成員類型的共有屬性。
示例:
interface Bird {fly(): void;layEggs(): void;
}interface Fish {swim(): void;layEggs(): void;
}function getSmallPet(): Bird | Fish {// ...
}let pet = getSmallPet();
pet.layEggs(); // 正確
// pet.fly(); // 錯誤: Fish 類型沒有 fly 方法
八、交叉類型
交叉類型表示一個值同時符合幾種類型,使用 &
分隔。
示例:
interface Person {name: string;
}interface Employee {employeeId: number;
}type PersonEmployee = Person & Employee;let personEmployee: PersonEmployee = {name: "Alice",employeeId: 123
};
8.1 交叉類型字面量
交叉類型字面量是指字面量值的交叉。
示例:
type Color = "red" & "blue"; // 實際上是一個空類型,因為沒有值同時是 "red" 和 "blue"let color: Color;
// 無法實例化,因為沒有值符合 Color 類型
8.2 交叉類型的類型成員
交叉類型的成員類型必須符合所有成員類型的屬性。
示例:
interface A {a: string;
}interface B {b: number;
}type C = A & B;let c: C = {a: "Hello",b: 42
};
九、交叉類型與聯合類型
交叉類型和聯合類型可以組合使用,以創建更復雜的類型。
示例:
interface Dog {bark(): void;
}interface Cat {meow(): void;
}type DogCat = Dog & Cat;function getDogCat(): DogCat {return {bark() {console.log("Woof!");},meow() {console.log("Meow!");}};
}let pet: DogCat = getDogCat();
pet.bark(); // 輸出: Woof!
pet.meow(); // 輸出: Meow!
十、索引類型
索引類型允許在泛型中使用動態屬性訪問。
10.1 索引類型查詢
使用 keyof
操作符獲取一個類型的鍵的聯合類型。
示例:
interface Person {name: string;age: number;address: string;
}type PersonKeys = keyof Person; // "name" | "age" | "address"
10.2 索引訪問類型
使用索引訪問類型獲取某個屬性的類型。
示例:
type NameType = Person["name"]; // string
type AgeType = Person["age"]; // number
10.3 索引類型的應用
結合泛型和索引類型,實現更靈活的類型操作。
示例:
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {return obj[key];
}let person: Person = {name: "Bob",age: 25,address: "123 Main St"
};let name = getProperty(person, "name"); // name 的類型為 string
let age = getProperty(person, "age"); // age 的類型為 number
十一、映射對象類型
映射對象類型允許動態地創建對象類型,基于已有的類型進行轉換。
11.1 映射對象類型聲明
使用 in
關鍵字和 keyof
操作符來聲明映射對象類型。
示例:
type ReadonlyPerson = {readonly [K in keyof Person]: Person[K]
};let readonlyPerson: ReadonlyPerson = {name: "Charlie",age: 30,address: "456 Elm St"
};// readonlyPerson.age = 31; // 錯誤: 屬性 age 是只讀的
11.2 映射對象類型解析
映射對象類型通過遍歷鍵并應用轉換函數來生成新的類型。
示例:
type Stringify<T> = {[K in keyof T]: string;
};type StringifiedPerson = Stringify<Person>;let stringifiedPerson: StringifiedPerson = {name: "Dave",age: "25",address: "789 Oak St"
};
11.3 映射對象類型的應用
結合泛型和映射對象類型,實現更復雜的類型操作。
示例:
// 定義一個泛型函數,將對象的屬性值轉換為字符串
function stringifyValues<T>(obj: T): { [K in keyof T]: string } {let result = {} as { [K in keyof T]: string };for (let key in obj) {result[key] = String(obj[key]);}return result;
}let person: Person = {name: "Eve",age: 22,address: "321 Pine St"
};let stringifiedPerson = stringifyValues(person);
// stringifiedPerson 的類型為 { name: string; age: string; address: string }
十二、同態映射對象類型
同態映射對象類型是指在映射過程中保持原有類型的結構。
示例:
// 定義一個泛型函數,創建一個只讀版本的映射對象類型
function makeReadonly<T>(obj: T): { readonly [K in keyof T]: T[K] } {return Object.freeze(obj);
}let person: Person = {name: "Frank",age: 28,address: "654 Cedar St"
};let readonlyPerson = makeReadonly(person);
// readonlyPerson 的屬性是只讀的
// readonlyPerson.age = 29; // 錯誤: 屬性 age 是只讀的
總結
TypeScript 的泛型與類型操作提供了強大的工具,使得開發者能夠編寫靈活、可復用的代碼。通過理解和掌握這些概念,可以顯著提升代碼質量和開發效率。
案例代碼匯總
以下是上述各個部分的完整代碼示例:
// 泛型函數
function identity<T>(arg: T): T {return arg;
}let output = identity<number>(42);
let output2 = identity("Hello, TypeScript!");// 泛型約束
interface Lengthwise {length: number;
}function loggingIdentity<T extends Lengthwise>(arg: T): T {console.log(arg.length);return arg;
}// 泛型接口
interface Pair<K, V> {key: K;value: V;
}let user: Pair<string, number> = {key: "age",value: 30
};// 泛型類型別名
type Container<T> = T[] | { value: T };let numberContainer: Container<number> = [1, 2, 3];
let stringContainer: Container<string> = { value: "Hello" };// 泛型類
class Stack<T> {private elements: T[] = [];push(element: T) {this.elements.push(element);}pop(): T | undefined {return this.elements.pop();}peek(): T | undefined {return this.elements[this.elements.length - 1];}
}let numberStack = new Stack<number>();
numberStack.push(1);
numberStack.push(2);
console.log(numberStack.peek());let stringStack = new Stack<string>();
stringStack.push("a");
stringStack.push("b");
console.log(stringStack.peek());// 局部類型
function process<T>(value: T) {type Wrapped = { wrapped: T };let wrappedValue: Wrapped = { wrapped: value };console.log(wrappedValue);
}process("Hello");// 聯合類型
function printId(id: number | string) {console.log("ID:", id);
}printId(101);
printId("202");// 聯合類型字面量
type Direction = "left" | "right" | "up" | "down";function move(direction: Direction) {console.log("Moving", direction);
}move("left");// 交叉類型
interface Person {name: string;
}interface Employee {employeeId: number;
}type PersonEmployee = Person & Employee;let personEmployee: PersonEmployee = {name: "Alice",employeeId: 123
};// 索引類型
interface Person {name: string;age: number;address: string;
}type PersonKeys = keyof Person;function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {return obj[key];
}let person: Person = {name: "Bob",age: 25,address: "123 Main St"
};let name = getProperty(person, "name");
let age = getProperty(person, "age");// 映射對象類型
type ReadonlyPerson = {readonly [K in keyof Person]: Person[K]
};let readonlyPerson: ReadonlyPerson = {name: "Charlie",age: 30,address: "456 Elm St"
};// 映射對象類型解析
type Stringify<T> = {[K in keyof T]: string;
};type StringifiedPerson = Stringify<Person>;let stringifiedPerson: StringifiedPerson = {name: "Dave",age: "25",address: "789 Oak St"
};// 映射對象類型的應用
function stringifyValues<T>(obj: T): { [K in keyof T]: string } {let result = {} as { [K in keyof T]: string };for (let key in obj) {result[key] = String(obj[key]);}return result;
}let person: Person = {name: "Eve",age: 22,address: "321 Pine St"
};let stringifiedPerson2 = stringifyValues(person);// 同態映射對象類型
function makeReadonly<T>(obj: T): { readonly [K in keyof T]: T[K] } {return Object.freeze(obj);
}let person2: Person = {name: "Frank",age: 28,address: "654 Cedar St"
};let readonlyPerson2 = makeReadonly(person2);